mcap/modules/Screen_Record/remux.jai
Musa Mahmood e0eaf5642b [0.1.7] Recordings go into the library as regular MP4s, with a keyframe every 2 s
- A recording killed mid-way had wrong timestamps in players: its fragmented MP4's header gives a
  length of 0, and one cut short never gets the index a clean stop writes. modules/Screen_Record's
  record_remux now copies every recording's packets, as they are, into a regular MP4 (one header
  first with the length and every frame's index; faststart) before it goes into the library;
  should that fail, the fragmented MP4 goes in as it was.
- Keyframes were every 60 frames, which, with frames written only on change, could span minutes
  of time, and a crash could lose a fragment that long. One is now forced every 2 s of time
  (forced-idr for NVENC and libx264).
- A crash's leftover no longer claims a region at 0, 0 in its capture.json.
- record-probe -remux IN OUT. Tests: a recording cut at its last fragment, as a crash cuts it,
  goes in as a regular MP4 of the time before the cut.
- Checked on the recording that showed it: its MP4, cut mid-fragment, rewrites to 76 frames
  (5.2 s) with positive frame durations. A 12 s still recording now fragments every ~2 s, and
  the app killed 10 s into a recording leaves 8.1 s.
2026-10-10 07:54:48 +01:00

66 lines
3.4 KiB
Plaintext

// A recording made into a regular MP4. While recording, the MP4 is written fragmented (encoder.jai), so
// a crash still leaves one that plays; but a fragmented MP4's header says nothing of its length or
// where its frames are, and one cut short by a crash never gets the index written at the end of a
// clean stop, so players show its time and seek in it badly. record_remux copies its packets, as they
// are, into a regular MP4: one header, with the length and the index of every frame, at the front
// (faststart), so it plays, seeks and streams as any MP4 does. Nothing is encoded again.
// Copies source's video and audio packets into a regular MP4 at destination. packets: how many.
record_remux :: (source: string, destination: string) -> ok: bool, error: string, packets: s64 {
av_log_set_level(AV_LOG_ERROR);
input: *AVFormatContext;
result := avformat_open_input(*input, temp_c_string(source), null, null);
if result < 0 return false, av_error("FFmpeg cannot open the recording", result), 0;
defer avformat_close_input(*input);
result = avformat_find_stream_info(input, null);
if result < 0 return false, av_error("FFmpeg cannot read the recording's streams", result), 0;
output: *AVFormatContext;
result = avformat_alloc_output_context2(*output, null, "mp4", temp_c_string(destination));
if result < 0 || !output return false, av_error("FFmpeg cannot make an MP4", result), 0;
defer {
if output.pb avio_closep(*output.pb);
avformat_free_context(output);
}
// Each input stream's output stream, or -1 for one left out (neither video nor audio).
map := NewArray(input.nb_streams, s32,, temp);
for 0..input.nb_streams - 1 {
in := input.streams[it];
map[it] = -1;
if in.codecpar.codec_type != .VIDEO && in.codecpar.codec_type != .AUDIO continue;
out := avformat_new_stream(output, null);
if !out return false, "FFmpeg cannot add a stream", 0;
avcodec_parameters_copy(out.codecpar, in.codecpar);
out.codecpar.codec_tag = 0; // the MP4 muxer picks its own
out.time_base = in.time_base;
map[it] = out.index;
}
result = avio_open(*output.pb, temp_c_string(destination), AVIO_FLAG_WRITE);
if result < 0 return false, av_error(tprint("FFmpeg cannot write %", destination), result), 0;
options: *AVDictionary;
av_dict_set(*options, "movflags", "+faststart", 0); // the header before the frames: a second pass at the end
result = avformat_write_header(output, *options);
av_dict_free(*options);
if result < 0 return false, av_error("FFmpeg cannot start the MP4", result), 0;
packet := av_packet_alloc();
defer av_packet_free(*packet);
packets: s64;
while av_read_frame(input, packet) >= 0 { // to the end, or to where a crash cut it
index := map[packet.stream_index];
if index < 0 { av_packet_unref(packet); continue; }
av_packet_rescale_ts(packet, input.streams[packet.stream_index].time_base, output.streams[index].time_base);
packet.stream_index = index;
packet.pos = -1;
result = av_interleaved_write_frame(output, packet); // takes the packet's data
if result < 0 return false, av_error("FFmpeg cannot write to the MP4", result), packets;
packets += 1;
}
if !packets return false, "the recording holds no frames", 0;
result = av_write_trailer(output);
if result < 0 return false, av_error("FFmpeg cannot finish the MP4", result), packets;
return true, "", packets;
}