forked from FFmpeg/FFmpeg
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:
parent
4b8ddf71dc
commit
6ec22731ae
6 changed files with 78 additions and 9 deletions
|
@ -569,6 +569,17 @@ experimental, may be renamed or changed, do not use from scripts.
|
||||||
|
|
||||||
@item write_gama
|
@item write_gama
|
||||||
write deprecated gama atom
|
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
|
@end table
|
||||||
|
|
||||||
@item movie_timescale @var{scale}
|
@item movie_timescale @var{scale}
|
||||||
|
|
|
@ -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" },
|
{ "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_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" },
|
{ "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},
|
{ "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},
|
{ "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},
|
{ "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;
|
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)
|
if (!track->entry)
|
||||||
return 0;
|
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->entry = 0;
|
||||||
track->entries_flushed = 0;
|
track->entries_flushed = 0;
|
||||||
track->end_reliable = 0;
|
track->end_reliable = 0;
|
||||||
|
@ -6008,7 +6029,7 @@ static int mov_flush_fragment(AVFormatContext *s, int force)
|
||||||
{
|
{
|
||||||
MOVMuxContext *mov = s->priv_data;
|
MOVMuxContext *mov = s->priv_data;
|
||||||
int i, first_track = -1;
|
int i, first_track = -1;
|
||||||
int64_t mdat_size = 0;
|
int64_t mdat_size = 0, mdat_start = 0;
|
||||||
int ret;
|
int ret;
|
||||||
int has_video = 0, starts_with_key = 0, first_video_track = 1;
|
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->moov_written = 1;
|
||||||
mov->mdat_size = 0;
|
mov->mdat_size = 0;
|
||||||
for (i = 0; i < mov->nb_tracks; i++)
|
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);
|
avio_write_marker(s->pb, AV_NOPTS_VALUE, AVIO_DATA_MARKER_FLUSH_POINT);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -6183,9 +6204,10 @@ static int mov_flush_fragment(AVFormatContext *s, int force)
|
||||||
|
|
||||||
avio_wb32(s->pb, mdat_size + 8);
|
avio_wb32(s->pb, mdat_size + 8);
|
||||||
ffio_wfourcc(s->pb, "mdat");
|
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 (!mov->frag_interleave) {
|
||||||
if (!track->mdat_buf)
|
if (!track->mdat_buf)
|
||||||
continue;
|
continue;
|
||||||
|
@ -7170,6 +7192,7 @@ static void mov_free(AVFormatContext *s)
|
||||||
else if (track->tag == MKTAG('t','m','c','d') && mov->nb_meta_tmcd)
|
else if (track->tag == MKTAG('t','m','c','d') && mov->nb_meta_tmcd)
|
||||||
av_freep(&track->par);
|
av_freep(&track->par);
|
||||||
av_freep(&track->cluster);
|
av_freep(&track->cluster);
|
||||||
|
av_freep(&track->cluster_written);
|
||||||
av_freep(&track->frag_info);
|
av_freep(&track->frag_info);
|
||||||
av_packet_free(&track->cover_image);
|
av_packet_free(&track->cover_image);
|
||||||
|
|
||||||
|
@ -7364,6 +7387,9 @@ static int mov_init(AVFormatContext *s)
|
||||||
mov->flags |= FF_MOV_FLAG_FRAGMENT;
|
mov->flags |= FF_MOV_FLAG_FRAGMENT;
|
||||||
|
|
||||||
/* Set other implicit flags immediately */
|
/* Set other implicit flags immediately */
|
||||||
|
if (mov->flags & FF_MOV_FLAG_HYBRID_FRAGMENTED)
|
||||||
|
mov->flags |= FF_MOV_FLAG_FRAGMENT;
|
||||||
|
|
||||||
if (mov->mode == MODE_ISM)
|
if (mov->mode == MODE_ISM)
|
||||||
mov->flags |= FF_MOV_FLAG_EMPTY_MOOV | FF_MOV_FLAG_SEPARATE_MOOF |
|
mov->flags |= FF_MOV_FLAG_EMPTY_MOOV | FF_MOV_FLAG_SEPARATE_MOOF |
|
||||||
FF_MOV_FLAG_FRAGMENT | FF_MOV_FLAG_NEGATIVE_CTS_OFFSETS;
|
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)) &&
|
FF_MOV_FLAG_FRAG_EVERY_FRAME)) &&
|
||||||
!mov->max_fragment_duration && !mov->max_fragment_size)
|
!mov->max_fragment_duration && !mov->max_fragment_size)
|
||||||
mov->flags |= FF_MOV_FLAG_FRAG_KEYFRAME;
|
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) {
|
} else if (mov->mode != MODE_AVIF) {
|
||||||
if (mov->flags & FF_MOV_FLAG_FASTSTART)
|
if (mov->flags & FF_MOV_FLAG_FASTSTART)
|
||||||
mov->reserved_header_pos = avio_tell(pb);
|
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);
|
moov_pos = avio_tell(pb);
|
||||||
|
|
||||||
/* Write size of mdat tag */
|
/* Write size of mdat tag */
|
||||||
if (mov->mdat_size + 8 <= UINT32_MAX) {
|
if (mov->mdat_size + 8 <= UINT32_MAX) {
|
||||||
avio_seek(pb, mov->mdat_pos, SEEK_SET);
|
avio_seek(pb, mov->mdat_pos, SEEK_SET);
|
||||||
avio_wb32(pb, mov->mdat_size + 8);
|
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 {
|
} else {
|
||||||
/* overwrite 'wide' placeholder atom */
|
/* overwrite 'wide' placeholder atom */
|
||||||
avio_seek(pb, mov->mdat_pos - 8, SEEK_SET);
|
avio_seek(pb, mov->mdat_pos - 8, SEEK_SET);
|
||||||
|
|
|
@ -85,7 +85,7 @@ typedef struct MOVFragmentInfo {
|
||||||
|
|
||||||
typedef struct MOVTrack {
|
typedef struct MOVTrack {
|
||||||
int mode;
|
int mode;
|
||||||
int entry;
|
int entry, entry_written;
|
||||||
unsigned timescale;
|
unsigned timescale;
|
||||||
uint64_t time;
|
uint64_t time;
|
||||||
int64_t track_duration;
|
int64_t track_duration;
|
||||||
|
@ -114,6 +114,7 @@ typedef struct MOVTrack {
|
||||||
int vos_len;
|
int vos_len;
|
||||||
uint8_t *vos_data;
|
uint8_t *vos_data;
|
||||||
MOVIentry *cluster;
|
MOVIentry *cluster;
|
||||||
|
MOVIentry *cluster_written;
|
||||||
unsigned cluster_capacity;
|
unsigned cluster_capacity;
|
||||||
int audio_vbr;
|
int audio_vbr;
|
||||||
int height; ///< active picture (w/o VBI) height for D-10/IMX
|
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_SKIP_SIDX (1 << 21)
|
||||||
#define FF_MOV_FLAG_CMAF (1 << 22)
|
#define FF_MOV_FLAG_CMAF (1 << 22)
|
||||||
#define FF_MOV_FLAG_PREFER_ICC (1 << 23)
|
#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);
|
int ff_mov_write_packet(AVFormatContext *s, AVPacket *pkt);
|
||||||
|
|
||||||
|
|
|
@ -31,8 +31,8 @@
|
||||||
|
|
||||||
#include "version_major.h"
|
#include "version_major.h"
|
||||||
|
|
||||||
#define LIBAVFORMAT_VERSION_MINOR 3
|
#define LIBAVFORMAT_VERSION_MINOR 4
|
||||||
#define LIBAVFORMAT_VERSION_MICRO 104
|
#define LIBAVFORMAT_VERSION_MICRO 100
|
||||||
|
|
||||||
#define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \
|
#define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \
|
||||||
LIBAVFORMAT_VERSION_MINOR, \
|
LIBAVFORMAT_VERSION_MINOR, \
|
||||||
|
|
|
@ -5,7 +5,7 @@ FATE_LAVF_CONTAINER-$(call ENCDEC, FLV, FLV) +
|
||||||
FATE_LAVF_CONTAINER-$(call ENCDEC, RAWVIDEO, FILMSTRIP) += flm
|
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, 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, 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 ENCDEC, MPEG4, MOV) += mp4
|
||||||
FATE_LAVF_CONTAINER-$(call ENCDEC2, MPEG1VIDEO, MP2, MPEG1SYSTEM MPEGPS) += mpg
|
FATE_LAVF_CONTAINER-$(call ENCDEC2, MPEG1VIDEO, MP2, MPEG1SYSTEM MPEGPS) += mpg
|
||||||
FATE_LAVF_CONTAINER-$(call ENCDEC , FFV1, MXF) += mxf_ffv1
|
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-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: 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_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-mp4: CMD = lavf_container_timecode "-c:v mpeg4 -an -threads 1"
|
||||||
fate-lavf-mpg: CMD = lavf_container_timecode "-ar 44100 -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"
|
fate-lavf-mxf: CMD = lavf_container_timecode "-af aresample=48000:tsf=s16p -bf 2 -threads 1"
|
||||||
|
|
3
tests/ref/lavf/mov_hybrid_frag
Normal file
3
tests/ref/lavf/mov_hybrid_frag
Normal 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
|
Loading…
Add table
Reference in a new issue