movenc: Add an option for resilient, hybrid fragmented/non-fragmented muxing

This allows ending up with a normal, non-fragmented file when
the file is finished, while keeping the file readable if writing
is aborted abruptly at any point. (Normally when writing a
mov/mp4 file, the unfinished file is completely useless unless it
is finished properly.)

This results in a file where the mdat atom contains (and hides)
all the moof atoms that were part of the fragmented file structure
initially.

Signed-off-by: Martin Storsjö <martin@martin.st>
This commit is contained in:
Martin Storsjö 2014-11-02 01:02:43 +02:00
parent 4b8ddf71dc
commit 6ec22731ae
6 changed files with 78 additions and 9 deletions

View File

@ -569,6 +569,17 @@ experimental, may be renamed or changed, do not use from scripts.
@item write_gama
write deprecated gama atom
@item hybrid_fragmented
For recoverability - write the output file as a fragmented file.
This allows the intermediate file to be read while being written
(in particular, if the writing process is aborted uncleanly). When
writing is finished, the file is converted to a regular, non-fragmented
file, which is more compatible and allows easier and quicker seeking.
If writing is aborted, the intermediate file can manually be
remuxed to get a regular, non-fragmented file of what had been
written into the unfinished file.
@end table
@item movie_timescale @var{scale}

View File

@ -111,6 +111,7 @@ static const AVOption options[] = {
{ "use_metadata_tags", "Use mdta atom for metadata.", 0, AV_OPT_TYPE_CONST, {.i64 = FF_MOV_FLAG_USE_MDTA}, INT_MIN, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM, .unit = "movflags" },
{ "write_colr", "Write colr atom even if the color info is unspecified (Experimental, may be renamed or changed, do not use from scripts)", 0, AV_OPT_TYPE_CONST, {.i64 = FF_MOV_FLAG_WRITE_COLR}, INT_MIN, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM, .unit = "movflags" },
{ "write_gama", "Write deprecated gama atom", 0, AV_OPT_TYPE_CONST, {.i64 = FF_MOV_FLAG_WRITE_GAMA}, INT_MIN, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM, .unit = "movflags" },
{ "hybrid_fragmented", "For recoverability, write a fragmented file that is converted to non-fragmented at the end.", 0, AV_OPT_TYPE_CONST, {.i64 = FF_MOV_FLAG_HYBRID_FRAGMENTED}, INT_MIN, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM, .unit = "movflags" },
{ "min_frag_duration", "Minimum fragment duration", offsetof(MOVMuxContext, min_fragment_duration), AV_OPT_TYPE_INT, {.i64 = 0}, 0, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM},
{ "mov_gamma", "gamma value for gama atom", offsetof(MOVMuxContext, gamma), AV_OPT_TYPE_FLOAT, {.dbl = 0.0 }, 0.0, 10, AV_OPT_FLAG_ENCODING_PARAM},
{ "movie_timescale", "set movie timescale", offsetof(MOVMuxContext, movie_timescale), AV_OPT_TYPE_INT, {.i64 = MOV_TIMESCALE}, 1, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM},
@ -5994,10 +5995,30 @@ static int mov_write_squashed_packets(AVFormatContext *s)
return 0;
}
static int mov_finish_fragment(MOVTrack *track)
static int mov_finish_fragment(MOVMuxContext *mov, MOVTrack *track,
int64_t ref_pos)
{
int i;
if (!track->entry)
return 0;
if (mov->flags & FF_MOV_FLAG_HYBRID_FRAGMENTED) {
for (i = 0; i < track->entry; i++)
track->cluster[i].pos += ref_pos + track->data_offset;
if (track->cluster_written == 0 && !(mov->flags & FF_MOV_FLAG_EMPTY_MOOV)) {
// First flush. If this was a case of not using empty moov, reset chunking.
for (i = 0; i < track->entry; i++) {
track->cluster[i].chunkNum = 0;
track->cluster[i].samples_in_chunk = track->cluster[i].entries;
}
}
if (av_reallocp_array(&track->cluster_written,
track->entry_written + track->entry,
sizeof(*track->cluster)))
return AVERROR(ENOMEM);
memcpy(&track->cluster_written[track->entry_written],
track->cluster, track->entry * sizeof(*track->cluster));
track->entry_written += track->entry;
}
track->entry = 0;
track->entries_flushed = 0;
track->end_reliable = 0;
@ -6008,7 +6029,7 @@ static int mov_flush_fragment(AVFormatContext *s, int force)
{
MOVMuxContext *mov = s->priv_data;
int i, first_track = -1;
int64_t mdat_size = 0;
int64_t mdat_size = 0, mdat_start = 0;
int ret;
int has_video = 0, starts_with_key = 0, first_video_track = 1;
@ -6114,7 +6135,7 @@ static int mov_flush_fragment(AVFormatContext *s, int force)
mov->moov_written = 1;
mov->mdat_size = 0;
for (i = 0; i < mov->nb_tracks; i++)
mov_finish_fragment(&mov->tracks[i]);
mov_finish_fragment(mov, &mov->tracks[i], 0);
avio_write_marker(s->pb, AV_NOPTS_VALUE, AVIO_DATA_MARKER_FLUSH_POINT);
return 0;
}
@ -6183,9 +6204,10 @@ static int mov_flush_fragment(AVFormatContext *s, int force)
avio_wb32(s->pb, mdat_size + 8);
ffio_wfourcc(s->pb, "mdat");
mdat_start = avio_tell(s->pb);
}
mov_finish_fragment(&mov->tracks[i]);
mov_finish_fragment(mov, &mov->tracks[i], mdat_start);
if (!mov->frag_interleave) {
if (!track->mdat_buf)
continue;
@ -7170,6 +7192,7 @@ static void mov_free(AVFormatContext *s)
else if (track->tag == MKTAG('t','m','c','d') && mov->nb_meta_tmcd)
av_freep(&track->par);
av_freep(&track->cluster);
av_freep(&track->cluster_written);
av_freep(&track->frag_info);
av_packet_free(&track->cover_image);
@ -7364,6 +7387,9 @@ static int mov_init(AVFormatContext *s)
mov->flags |= FF_MOV_FLAG_FRAGMENT;
/* Set other implicit flags immediately */
if (mov->flags & FF_MOV_FLAG_HYBRID_FRAGMENTED)
mov->flags |= FF_MOV_FLAG_FRAGMENT;
if (mov->mode == MODE_ISM)
mov->flags |= FF_MOV_FLAG_EMPTY_MOOV | FF_MOV_FLAG_SEPARATE_MOOF |
FF_MOV_FLAG_FRAGMENT | FF_MOV_FLAG_NEGATIVE_CTS_OFFSETS;
@ -7888,6 +7914,11 @@ static int mov_write_header(AVFormatContext *s)
FF_MOV_FLAG_FRAG_EVERY_FRAME)) &&
!mov->max_fragment_duration && !mov->max_fragment_size)
mov->flags |= FF_MOV_FLAG_FRAG_KEYFRAME;
if (mov->flags & FF_MOV_FLAG_HYBRID_FRAGMENTED) {
avio_wb32(pb, 8); // placeholder for extended size field (64 bit)
ffio_wfourcc(pb, mov->mode == MODE_MOV ? "wide" : "free");
mov->mdat_pos = avio_tell(pb);
}
} else if (mov->mode != MODE_AVIF) {
if (mov->flags & FF_MOV_FLAG_FASTSTART)
mov->reserved_header_pos = avio_tell(pb);
@ -8091,13 +8122,34 @@ static int mov_write_trailer(AVFormatContext *s)
}
}
if (!(mov->flags & FF_MOV_FLAG_FRAGMENT)) {
if (!(mov->flags & FF_MOV_FLAG_FRAGMENT) ||
mov->flags & FF_MOV_FLAG_HYBRID_FRAGMENTED) {
if (mov->flags & FF_MOV_FLAG_HYBRID_FRAGMENTED) {
mov_flush_fragment(s, 1);
mov->mdat_size = avio_tell(pb) - mov->mdat_pos - 8;
for (i = 0; i < mov->nb_tracks; i++) {
MOVTrack *track = &mov->tracks[i];
track->data_offset = 0;
av_free(track->cluster);
track->cluster = track->cluster_written;
track->entry = track->entry_written;
track->cluster_written = NULL;
track->entry_written = 0;
track->chunkCount = 0; // Force build_chunks to rebuild the list of chunks
}
// Clear the empty_moov flag, as we do want the moov to include
// all the samples at this point.
mov->flags &= ~FF_MOV_FLAG_EMPTY_MOOV;
}
moov_pos = avio_tell(pb);
/* Write size of mdat tag */
if (mov->mdat_size + 8 <= UINT32_MAX) {
avio_seek(pb, mov->mdat_pos, SEEK_SET);
avio_wb32(pb, mov->mdat_size + 8);
if (mov->flags & FF_MOV_FLAG_HYBRID_FRAGMENTED)
ffio_wfourcc(pb, "mdat"); // overwrite the original moov into a mdat
} else {
/* overwrite 'wide' placeholder atom */
avio_seek(pb, mov->mdat_pos - 8, SEEK_SET);

View File

@ -85,7 +85,7 @@ typedef struct MOVFragmentInfo {
typedef struct MOVTrack {
int mode;
int entry;
int entry, entry_written;
unsigned timescale;
uint64_t time;
int64_t track_duration;
@ -114,6 +114,7 @@ typedef struct MOVTrack {
int vos_len;
uint8_t *vos_data;
MOVIentry *cluster;
MOVIentry *cluster_written;
unsigned cluster_capacity;
int audio_vbr;
int height; ///< active picture (w/o VBI) height for D-10/IMX
@ -282,6 +283,7 @@ typedef struct MOVMuxContext {
#define FF_MOV_FLAG_SKIP_SIDX (1 << 21)
#define FF_MOV_FLAG_CMAF (1 << 22)
#define FF_MOV_FLAG_PREFER_ICC (1 << 23)
#define FF_MOV_FLAG_HYBRID_FRAGMENTED (1 << 24)
int ff_mov_write_packet(AVFormatContext *s, AVPacket *pkt);

View File

@ -31,8 +31,8 @@
#include "version_major.h"
#define LIBAVFORMAT_VERSION_MINOR 3
#define LIBAVFORMAT_VERSION_MICRO 104
#define LIBAVFORMAT_VERSION_MINOR 4
#define LIBAVFORMAT_VERSION_MICRO 100
#define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \
LIBAVFORMAT_VERSION_MINOR, \

View File

@ -5,7 +5,7 @@ FATE_LAVF_CONTAINER-$(call ENCDEC, FLV, FLV) +
FATE_LAVF_CONTAINER-$(call ENCDEC, RAWVIDEO, FILMSTRIP) += flm
FATE_LAVF_CONTAINER-$(call ENCDEC2, MPEG2VIDEO, PCM_S16LE, GXF) += gxf gxf_pal gxf_ntsc
FATE_LAVF_CONTAINER-$(call ENCDEC2, MPEG4, MP2, MATROSKA) += mkv mkv_attachment
FATE_LAVF_CONTAINER-$(call ENCDEC2, MPEG4, PCM_ALAW, MOV) += mov mov_rtphint ismv
FATE_LAVF_CONTAINER-$(call ENCDEC2, MPEG4, PCM_ALAW, MOV) += mov mov_rtphint mov_hybrid_frag ismv
FATE_LAVF_CONTAINER-$(call ENCDEC, MPEG4, MOV) += mp4
FATE_LAVF_CONTAINER-$(call ENCDEC2, MPEG1VIDEO, MP2, MPEG1SYSTEM MPEGPS) += mpg
FATE_LAVF_CONTAINER-$(call ENCDEC , FFV1, MXF) += mxf_ffv1
@ -51,6 +51,7 @@ fate-lavf-mkv: CMD = lavf_container "" "-c:a mp2 -c:v mpeg4 -ar 44100 -threads 1
fate-lavf-mkv_attachment: CMD = lavf_container_attach "-c:a mp2 -c:v mpeg4 -threads 1 -f matroska"
fate-lavf-mov: CMD = lavf_container_timecode "-movflags +faststart -c:a pcm_alaw -c:v mpeg4 -threads 1"
fate-lavf-mov_rtphint: CMD = lavf_container "" "-movflags +rtphint -c:a pcm_alaw -c:v mpeg4 -threads 1 -f mov"
fate-lavf-mov_hybrid_frag: CMD = lavf_container "" "-movflags +hybrid_fragmented -c:a pcm_alaw -c:v mpeg4 -threads 1 -f mov"
fate-lavf-mp4: CMD = lavf_container_timecode "-c:v mpeg4 -an -threads 1"
fate-lavf-mpg: CMD = lavf_container_timecode "-ar 44100 -threads 1"
fate-lavf-mxf: CMD = lavf_container_timecode "-af aresample=48000:tsf=s16p -bf 2 -threads 1"

View File

@ -0,0 +1,3 @@
4871796f41234350f1b050317d0288a3 *tests/data/lavf/lavf.mov_hybrid_frag
358508 tests/data/lavf/lavf.mov_hybrid_frag
tests/data/lavf/lavf.mov_hybrid_frag CRC=0xbb2b949b