This page documents sc_recorder, the client-side component responsible for multiplexing video and audio packets into a local file. It covers the sc_packet_sink interfaces it implements, its threading model, supported output formats, timestamp normalization, and orientation metadata handling.
For how packets arrive at the recorder (demuxing and decoding), see the Media Processing Pipeline (2.5). For how the V4L2 sink consumes decoded frames instead of raw packets, see V4L2 Integration (2.7).
sc_recorder operates as a packet consumer: it receives encoded AVPacket objects directly from the demuxer (before decoding), which means recording has no transcoding cost. It implements two sc_packet_sink interfaces — one for video, one for audio — and writes the packets to a file using libavformat's muxing API.
The recorder runs on its own dedicated background thread to avoid blocking the media pipeline.
Sources: app/src/recorder.h1-88 app/src/recorder.c1-20
The format is selected at initialization via enum sc_record_format (defined in options.h). The sc_recorder_get_format_name() function maps each format enum to a libavformat muxer name.
sc_record_format enum | libavformat muxer name | Typical use |
|---|---|---|
SC_RECORD_FORMAT_MP4 | mp4 | Video + audio container |
SC_RECORD_FORMAT_M4A | mp4 | Audio-only (AAC in MP4) |
SC_RECORD_FORMAT_AAC | mp4 | Audio-only (AAC) |
SC_RECORD_FORMAT_MKV | matroska | Video + audio container |
SC_RECORD_FORMAT_MKA | matroska | Audio-only (MKV audio) |
SC_RECORD_FORMAT_OPUS | opus | Audio-only (Opus) |
SC_RECORD_FORMAT_FLAC | flac | Audio-only (FLAC) |
SC_RECORD_FORMAT_WAV | wav | Audio-only (raw PCM) |
Sources: app/src/recorder.c64-83
sc_recorder — the central struct declared in recorder.h:
| Field | Type | Purpose |
|---|---|---|
video_packet_sink | sc_packet_sink | Packet sink interface for video |
audio_packet_sink | sc_packet_sink | Packet sink interface for audio |
video / audio | bool | Whether each stream is active |
orientation | sc_orientation | Display rotation for output metadata |
filename | char * | Output file path |
format | sc_record_format | Output container format |
ctx | AVFormatContext * | libavformat muxer context |
thread | sc_thread | Background recorder thread |
mutex / cond | sc_mutex / sc_cond | Queue synchronization |
stopped | bool | Set to stop the thread |
video_queue | sc_recorder_queue | Pending video AVPacket * deque |
audio_queue | sc_recorder_queue | Pending audio AVPacket * deque |
video_init / audio_init | bool | Whether stream codec is known |
audio_expects_config_packet | bool | False for raw PCM audio |
video_stream / audio_stream | sc_recorder_stream | Per-stream index and last PTS |
sc_recorder_stream tracks the stream index in the AVFormatContext and the last_pts value used to enforce monotonically increasing timestamps.
sc_recorder_queue is a SC_VECDEQUE(AVPacket *) — a dynamic double-ended queue of packet pointers.
Sources: app/src/recorder.h16-63
Component integration diagram — how sc_recorder fits into the client pipeline:
Sources: app/src/recorder.h23-26 app/src/recorder.c542-721
Internal struct layout — code entities:
Sources: app/src/recorder.h1-88 app/src/recorder.c792-811
sc_recorder_init() app/src/recorder.c745-821 accepts the filename, format, video/audio booleans, orientation, and a sc_recorder_callbacks pointer. It:
sc_mutex and sc_cond.video_queue and audio_queue deques.sc_packet_sink_ops vtables for video and audio sinks.The sc_recorder_callbacks struct has a single callback:
void (*on_ended)(struct sc_recorder *recorder, bool success, void *userdata);
This is called at the end of the recorder thread regardless of success or failure.
| Function | Description |
|---|---|
sc_recorder_start() | Spawns the run_recorder thread named "scrcpy-recorder" |
sc_recorder_stop() | Sets stopped = true and signals cond |
sc_recorder_join() | Blocks until the thread exits |
sc_recorder_destroy() | Frees filename, destroys mutex/cond |
Sources: app/src/recorder.c823-853
SC_THREAD_PRIORITY_LOW so it does not interfere with real-time playback.video_queue, audio_queue, stopped, video_init, and audio_init.stopped is set to true. The recorder thread drains remaining queued packets before exiting.Sources: app/src/recorder.c473-504 app/src/recorder.c284-459
The first packet for each stream has pts == AV_NOPTS_VALUE. This is a config packet containing codec extradata (e.g., H.264 SPS/PPS). Subsequent config packets (sent on device orientation changes) are discarded; the Android server prepends the updated config to the next data packet automatically.
The sc_recorder_process_header() function app/src/recorder.c199-282 blocks until both streams are initialized and their config packets have arrived. It then:
sc_recorder_set_extradata() to copy the config packet data into stream->codecpar->extradata.avformat_write_header().For raw PCM audio (AV_CODEC_ID_PCM_S16LE), audio_expects_config_packet is set to false and no config packet is awaited.
Sources: app/src/recorder.c86-99 app/src/recorder.c199-282 app/src/recorder.c659-662
All scrcpy packets carry timestamps in microseconds, expressed as SCRCPY_TIME_BASE = {1, 1000000}.
In sc_recorder_process_packets() app/src/recorder.c284-459:
pts_origin is computed as the minimum PTS of the first video and audio packets. All subsequent PTS values are offset by subtracting pts_origin, so the recording starts at timestamp zero.pkt->pts - pts_origin and pkt->dts = pkt->pts.sc_recorder_rescale_packet() calls av_packet_rescale_ts() to convert from SCRCPY_TIME_BASE to the stream's time_base before writing.Video packet duration is computed lazily: each video packet is held as video_pkt_previous until the next packet arrives. The duration is then: next_pts - current_pts. The last video packet receives an arbitrary duration of 100,000 microseconds (0.1 s).
Sources: app/src/recorder.c21 app/src/recorder.c101-104 app/src/recorder.c284-459
sc_recorder_write_stream() app/src/recorder.c106-121 checks that each packet's PTS is strictly greater than the previous. If not, it forces packet->pts = ++last_pts and sets dts = pts. A debug log is emitted when this correction is applied.
Sources: app/src/recorder.c106-121
When recorder->orientation != SC_ORIENTATION_0, sc_recorder_set_orientation() app/src/recorder.c506-540 attaches a AV_PKT_DATA_DISPLAYMATRIX side data block to the video stream's codec parameters. The rotation angle (multiples of 90°) is written using av_display_rotation_set().
The function has two code paths based on FFmpeg version:
av_packet_side_data_new() on stream->codecpar->coded_side_data.av_stream_new_side_data().Mirror orientations are explicitly excluded (asserted away); only pure rotations are stored.
Sources: app/src/recorder.c506-540 app/src/recorder.c568-577
sc_recorder exposes two sc_packet_sink instances — video_packet_sink and audio_packet_sink — each backed by a vtable of sc_packet_sink_ops.
| Callback | Video implementation | Audio implementation |
|---|---|---|
open | sc_recorder_video_packet_sink_open | sc_recorder_audio_packet_sink_open |
close | sc_recorder_video_packet_sink_close | sc_recorder_audio_packet_sink_close |
push | sc_recorder_video_packet_sink_push | sc_recorder_audio_packet_sink_push |
disable | (not set) | sc_recorder_audio_packet_sink_disable |
open — Called when the codec is known. Creates an AVStream via avformat_new_stream(), copies codec parameters via avcodec_parameters_from_context(), records the stream index, and signals cond to unblock the recorder thread.
push — Refs the incoming AVPacket with sc_recorder_packet_ref() (which calls av_packet_ref() on a newly allocated packet), assigns the stream index, appends to the relevant queue, and signals cond.
close — Sets stopped = true and signals cond. This causes the recorder thread to drain remaining packets and exit.
disable (audio only) — Called if the audio stream becomes unavailable (e.g., device does not support audio capture). Sets recorder->audio = false and audio_init = true so the recorder thread can proceed without audio.
Sources: app/src/recorder.c542-737
sc_recorder_open_output_file() app/src/recorder.c133-174:
find_muxer() — iterates libavformat's muxer list via av_muxer_iterate() (or the deprecated av_oformat_next() for older versions) until it finds one whose name list contains the target format name.AVFormatContext with avformat_alloc_context().avio_open() using a "file:" URL prefix.ctx->oformat and adds a "comment" metadata tag containing the scrcpy version string.sc_recorder_close_output_file() app/src/recorder.c176-180 calls avio_close() and avformat_free_context().
Sources: app/src/recorder.c133-180
Sources: app/src/recorder.c461-504 app/src/recorder.c199-459
Refresh this wiki
This wiki was recently refreshed. Please wait 3 days to refresh again.