- 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.
66 lines
3.4 KiB
Plaintext
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;
|
|
}
|