This patch adds support for WebM Live Muxing by adding a new WebM
Chunk muxer. It writes out live WebM Chunks which can be used for
playback using Live DASH Clients.
Please see muxers.texi for sample usage.
Signed-off-by: Vignesh Venkatasubramanian <vigneshv@google.com>
Signed-off-by: Michael Niedermayer <michaelni@gmx.at>
... | ... |
@@ -1268,4 +1268,47 @@ ffmpeg -f webm_dash_manifest -i video1.webm \ |
1268 | 1268 |
manifest.xml |
1269 | 1269 |
@end example |
1270 | 1270 |
|
1271 |
+@section webm_chunk |
|
1272 |
+ |
|
1273 |
+WebM Live Chunk Muxer. |
|
1274 |
+ |
|
1275 |
+This muxer writes out WebM headers and chunks as separate files which can be |
|
1276 |
+consumed by clients that support WebM Live streams via DASH. |
|
1277 |
+ |
|
1278 |
+@subsection Options |
|
1279 |
+ |
|
1280 |
+This muxer supports the following options: |
|
1281 |
+ |
|
1282 |
+@table @option |
|
1283 |
+@item chunk_start_index |
|
1284 |
+Index of the first chunk (defaults to 0). |
|
1285 |
+ |
|
1286 |
+@item header |
|
1287 |
+Filename of the header where the initialization data will be written. |
|
1288 |
+ |
|
1289 |
+@item audio_chunk_duration |
|
1290 |
+Duration of each audio chunk in milliseconds (defaults to 5000). |
|
1291 |
+@end table |
|
1292 |
+ |
|
1293 |
+@subsection Example |
|
1294 |
+@example |
|
1295 |
+ffmpeg -f v4l2 -i /dev/video0 \ |
|
1296 |
+ -f alsa -i hw:0 \ |
|
1297 |
+ -map 0:0 \ |
|
1298 |
+ -c:v libvpx-vp9 \ |
|
1299 |
+ -s 640x360 -keyint_min 30 -g 30 \ |
|
1300 |
+ -f webm_chunk \ |
|
1301 |
+ -header webm_live_video_360.hdr \ |
|
1302 |
+ -chunk_start_index 1 \ |
|
1303 |
+ webm_live_video_360_%d.chk \ |
|
1304 |
+ -map 1:0 \ |
|
1305 |
+ -c:a libvorbis \ |
|
1306 |
+ -b:a 128k \ |
|
1307 |
+ -f webm_chunk \ |
|
1308 |
+ -header webm_live_audio_128.hdr \ |
|
1309 |
+ -chunk_start_index 1 \ |
|
1310 |
+ -audio_chunk_duration 1000 \ |
|
1311 |
+ webm_live_audio_128_%d.chk |
|
1312 |
+@end example |
|
1313 |
+ |
|
1271 | 1314 |
@c man end MUXERS |
... | ... |
@@ -238,7 +238,7 @@ OBJS-$(CONFIG_MATROSKA_DEMUXER) += matroskadec.o matroska.o \ |
238 | 238 |
OBJS-$(CONFIG_MATROSKA_MUXER) += matroskaenc.o matroska.o \ |
239 | 239 |
isom.o avc.o hevc.o \ |
240 | 240 |
flacenc_header.o avlanguage.o vorbiscomment.o wv.o \ |
241 |
- webmdashenc.o |
|
241 |
+ webmdashenc.o webm_chunk.o |
|
242 | 242 |
OBJS-$(CONFIG_MD5_MUXER) += md5enc.o |
243 | 243 |
OBJS-$(CONFIG_MGSTS_DEMUXER) += mgsts.o |
244 | 244 |
OBJS-$(CONFIG_MICRODVD_DEMUXER) += microdvddec.o subtitles.o |
... | ... |
@@ -452,12 +452,13 @@ OBJS-$(CONFIG_WEBM_MUXER) += matroskaenc.o matroska.o \ |
452 | 452 |
isom.o avc.o hevc.o \ |
453 | 453 |
flacenc_header.o avlanguage.o \ |
454 | 454 |
wv.o vorbiscomment.o \ |
455 |
- webmdashenc.o |
|
455 |
+ webmdashenc.o webm_chunk.o |
|
456 | 456 |
OBJS-$(CONFIG_WEBM_DASH_MANIFEST_DEMUXER)+= matroskadec.o matroska.o \ |
457 | 457 |
isom.o rmsipr.o flac_picture.o \ |
458 | 458 |
oggparsevorbis.o vorbiscomment.o \ |
459 | 459 |
flac_picture.o replaygain.o |
460 | 460 |
OBJS-$(CONFIG_WEBM_DASH_MANIFEST_MUXER) += webmdashenc.o matroska.o |
461 |
+OBJS-$(CONFIG_WEBM_CHUNK_MUXER) += webm_chunk.o matroska.o |
|
461 | 462 |
OBJS-$(CONFIG_WEBP_MUXER) += webpenc.o |
462 | 463 |
OBJS-$(CONFIG_WEBVTT_DEMUXER) += webvttdec.o subtitles.o |
463 | 464 |
OBJS-$(CONFIG_WEBVTT_MUXER) += webvttenc.o |
... | ... |
@@ -316,6 +316,7 @@ void av_register_all(void) |
316 | 316 |
REGISTER_DEMUXER (WC3, wc3); |
317 | 317 |
REGISTER_MUXER (WEBM, webm); |
318 | 318 |
REGISTER_MUXDEMUX(WEBM_DASH_MANIFEST, webm_dash_manifest); |
319 |
+ REGISTER_MUXER (WEBM_CHUNK, webm_chunk); |
|
319 | 320 |
REGISTER_MUXER (WEBP, webp); |
320 | 321 |
REGISTER_MUXDEMUX(WEBVTT, webvtt); |
321 | 322 |
REGISTER_DEMUXER (WSAUD, wsaud); |
... | ... |
@@ -120,6 +120,7 @@ typedef struct MatroskaMuxContext { |
120 | 120 |
int64_t cluster_time_limit; |
121 | 121 |
int is_dash; |
122 | 122 |
int dash_track_number; |
123 |
+ int is_live; |
|
123 | 124 |
|
124 | 125 |
uint32_t chapter_id_offset; |
125 | 126 |
int wrote_chapters; |
... | ... |
@@ -1412,7 +1413,9 @@ static int mkv_write_header(AVFormatContext *s) |
1412 | 1412 |
// reserve space for the duration |
1413 | 1413 |
mkv->duration = 0; |
1414 | 1414 |
mkv->duration_offset = avio_tell(pb); |
1415 |
- put_ebml_void(pb, 11); // assumes double-precision float to be written |
|
1415 |
+ if (!mkv->is_live) { |
|
1416 |
+ put_ebml_void(pb, 11); // assumes double-precision float to be written |
|
1417 |
+ } |
|
1416 | 1418 |
end_ebml_master(pb, segment_info); |
1417 | 1419 |
|
1418 | 1420 |
ret = mkv_write_tracks(s); |
... | ... |
@@ -1436,7 +1439,7 @@ static int mkv_write_header(AVFormatContext *s) |
1436 | 1436 |
return ret; |
1437 | 1437 |
} |
1438 | 1438 |
|
1439 |
- if (!s->pb->seekable) |
|
1439 |
+ if (!s->pb->seekable && !mkv->is_live) |
|
1440 | 1440 |
mkv_write_seekhead(pb, mkv->main_seekhead); |
1441 | 1441 |
|
1442 | 1442 |
mkv->cues = mkv_start_cues(mkv->segment_offset); |
... | ... |
@@ -1955,7 +1958,9 @@ static int mkv_write_trailer(AVFormatContext *s) |
1955 | 1955 |
avio_seek(pb, currentpos, SEEK_SET); |
1956 | 1956 |
} |
1957 | 1957 |
|
1958 |
- end_ebml_master(pb, mkv->segment); |
|
1958 |
+ if (!mkv->is_live) { |
|
1959 |
+ end_ebml_master(pb, mkv->segment); |
|
1960 |
+ } |
|
1959 | 1961 |
av_freep(&mkv->tracks); |
1960 | 1962 |
av_freep(&mkv->cues->entries); |
1961 | 1963 |
av_freep(&mkv->cues); |
... | ... |
@@ -2019,6 +2024,7 @@ static const AVOption options[] = { |
2019 | 2019 |
{ "cluster_time_limit", "Store at most the provided number of milliseconds in a cluster.", OFFSET(cluster_time_limit), AV_OPT_TYPE_INT64, { .i64 = -1 }, -1, INT64_MAX, FLAGS }, |
2020 | 2020 |
{ "dash", "Create a WebM file conforming to WebM DASH specification", OFFSET(is_dash), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, 1, FLAGS }, |
2021 | 2021 |
{ "dash_track_number", "Track number for the DASH stream", OFFSET(dash_track_number), AV_OPT_TYPE_INT, { .i64 = 1 }, 0, 127, FLAGS }, |
2022 |
+ { "live", "Write files assuming it is a live stream.", OFFSET(is_live), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, 1, FLAGS }, |
|
2022 | 2023 |
{ "allow_raw_vfw", "allow RAW VFW mode", OFFSET(allow_raw_vfw), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, 1, FLAGS }, |
2023 | 2024 |
{ NULL }, |
2024 | 2025 |
}; |
... | ... |
@@ -30,7 +30,7 @@ |
30 | 30 |
#include "libavutil/version.h" |
31 | 31 |
|
32 | 32 |
#define LIBAVFORMAT_VERSION_MAJOR 56 |
33 |
-#define LIBAVFORMAT_VERSION_MINOR 29 |
|
33 |
+#define LIBAVFORMAT_VERSION_MINOR 30 |
|
34 | 34 |
#define LIBAVFORMAT_VERSION_MICRO 100 |
35 | 35 |
|
36 | 36 |
#define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \ |
37 | 37 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,262 @@ |
0 |
+/* |
|
1 |
+ * Copyright (c) 2015, Vignesh Venkatasubramanian |
|
2 |
+ * |
|
3 |
+ * This file is part of FFmpeg. |
|
4 |
+ * |
|
5 |
+ * FFmpeg is free software; you can redistribute it and/or |
|
6 |
+ * modify it under the terms of the GNU Lesser General Public |
|
7 |
+ * License as published by the Free Software Foundation; either |
|
8 |
+ * version 2.1 of the License, or (at your option) any later version. |
|
9 |
+ * |
|
10 |
+ * FFmpeg is distributed in the hope that it will be useful, |
|
11 |
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
12 |
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
|
13 |
+ * Lesser General Public License for more details. |
|
14 |
+ * |
|
15 |
+ * You should have received a copy of the GNU Lesser General Public |
|
16 |
+ * License along with FFmpeg; if not, write to the Free Software |
|
17 |
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
|
18 |
+ */ |
|
19 |
+ |
|
20 |
+/** |
|
21 |
+ * @file WebM Chunk Muxer |
|
22 |
+ * The chunk muxer enables writing WebM Live chunks where there is a header |
|
23 |
+ * chunk, followed by data chunks where each Cluster is written out as a Chunk. |
|
24 |
+ */ |
|
25 |
+ |
|
26 |
+#include <float.h> |
|
27 |
+#include <time.h> |
|
28 |
+ |
|
29 |
+#include "avformat.h" |
|
30 |
+#include "avio.h" |
|
31 |
+#include "internal.h" |
|
32 |
+ |
|
33 |
+#include "libavutil/avassert.h" |
|
34 |
+#include "libavutil/log.h" |
|
35 |
+#include "libavutil/opt.h" |
|
36 |
+#include "libavutil/avstring.h" |
|
37 |
+#include "libavutil/parseutils.h" |
|
38 |
+#include "libavutil/mathematics.h" |
|
39 |
+#include "libavutil/time.h" |
|
40 |
+#include "libavutil/time_internal.h" |
|
41 |
+#include "libavutil/timestamp.h" |
|
42 |
+ |
|
43 |
+typedef struct WebMChunkContext { |
|
44 |
+ const AVClass *class; |
|
45 |
+ int chunk_start_index; |
|
46 |
+ char *header_filename; |
|
47 |
+ int chunk_duration; |
|
48 |
+ int chunk_count; |
|
49 |
+ int chunk_index; |
|
50 |
+ uint64_t duration_written; |
|
51 |
+ int prev_pts; |
|
52 |
+ AVOutputFormat *oformat; |
|
53 |
+ AVFormatContext *avf; |
|
54 |
+} WebMChunkContext; |
|
55 |
+ |
|
56 |
+static int chunk_mux_init(AVFormatContext *s) |
|
57 |
+{ |
|
58 |
+ WebMChunkContext *wc = s->priv_data; |
|
59 |
+ AVFormatContext *oc; |
|
60 |
+ int ret; |
|
61 |
+ |
|
62 |
+ ret = avformat_alloc_output_context2(&wc->avf, wc->oformat, NULL, NULL); |
|
63 |
+ if (ret < 0) |
|
64 |
+ return ret; |
|
65 |
+ oc = wc->avf; |
|
66 |
+ |
|
67 |
+ oc->interrupt_callback = s->interrupt_callback; |
|
68 |
+ oc->max_delay = s->max_delay; |
|
69 |
+ av_dict_copy(&oc->metadata, s->metadata, 0); |
|
70 |
+ |
|
71 |
+ oc->priv_data = av_mallocz(oc->oformat->priv_data_size); |
|
72 |
+ if (!oc->priv_data) { |
|
73 |
+ avio_close(oc->pb); |
|
74 |
+ return AVERROR(ENOMEM); |
|
75 |
+ } |
|
76 |
+ *(const AVClass**)oc->priv_data = oc->oformat->priv_class; |
|
77 |
+ av_opt_set_defaults(oc->priv_data); |
|
78 |
+ av_opt_set_int(oc->priv_data, "dash", 1, 0); |
|
79 |
+ av_opt_set_int(oc->priv_data, "cluster_time_limit", wc->chunk_duration, 0); |
|
80 |
+ av_opt_set_int(oc->priv_data, "live", 1, 0); |
|
81 |
+ |
|
82 |
+ oc->streams = s->streams; |
|
83 |
+ oc->nb_streams = s->nb_streams; |
|
84 |
+ |
|
85 |
+ return 0; |
|
86 |
+} |
|
87 |
+ |
|
88 |
+static int set_chunk_filename(AVFormatContext *s, int is_header) |
|
89 |
+{ |
|
90 |
+ WebMChunkContext *wc = s->priv_data; |
|
91 |
+ AVFormatContext *oc = wc->avf; |
|
92 |
+ if (is_header) { |
|
93 |
+ if (!wc->header_filename) { |
|
94 |
+ return AVERROR(EINVAL); |
|
95 |
+ } |
|
96 |
+ av_strlcpy(oc->filename, wc->header_filename, strlen(wc->header_filename) + 1); |
|
97 |
+ } else { |
|
98 |
+ if (av_get_frame_filename(oc->filename, sizeof(oc->filename), |
|
99 |
+ s->filename, wc->chunk_index) < 0) { |
|
100 |
+ av_log(oc, AV_LOG_ERROR, "Invalid chunk filename template '%s'\n", s->filename); |
|
101 |
+ return AVERROR(EINVAL); |
|
102 |
+ } |
|
103 |
+ } |
|
104 |
+ return 0; |
|
105 |
+} |
|
106 |
+ |
|
107 |
+static int webm_chunk_write_header(AVFormatContext *s) |
|
108 |
+{ |
|
109 |
+ WebMChunkContext *wc = s->priv_data; |
|
110 |
+ AVFormatContext *oc = NULL; |
|
111 |
+ int ret; |
|
112 |
+ |
|
113 |
+ // DASH Streams can only have either one track per file. |
|
114 |
+ if (s->nb_streams != 1) { return AVERROR_INVALIDDATA; } |
|
115 |
+ |
|
116 |
+ wc->chunk_count = 0; |
|
117 |
+ wc->chunk_index = wc->chunk_start_index; |
|
118 |
+ wc->oformat = av_guess_format("webm", s->filename, "video/webm"); |
|
119 |
+ if (!wc->oformat) |
|
120 |
+ return AVERROR_MUXER_NOT_FOUND; |
|
121 |
+ |
|
122 |
+ ret = chunk_mux_init(s); |
|
123 |
+ if (ret < 0) |
|
124 |
+ return ret; |
|
125 |
+ oc = wc->avf; |
|
126 |
+ ret = set_chunk_filename(s, 1); |
|
127 |
+ if (ret< 0) |
|
128 |
+ return ret; |
|
129 |
+ ret = avio_open2(&oc->pb, oc->filename, AVIO_FLAG_WRITE, |
|
130 |
+ &s->interrupt_callback, NULL); |
|
131 |
+ if (ret < 0) |
|
132 |
+ return ret; |
|
133 |
+ |
|
134 |
+ oc->pb->seekable = 0; |
|
135 |
+ ret = oc->oformat->write_header(oc); |
|
136 |
+ if (ret < 0) |
|
137 |
+ return ret; |
|
138 |
+ avio_close(oc->pb); |
|
139 |
+ return 0; |
|
140 |
+} |
|
141 |
+ |
|
142 |
+static int chunk_start(AVFormatContext *s) |
|
143 |
+{ |
|
144 |
+ WebMChunkContext *wc = s->priv_data; |
|
145 |
+ AVFormatContext *oc = wc->avf; |
|
146 |
+ int ret; |
|
147 |
+ |
|
148 |
+ ret = set_chunk_filename(s, 0); |
|
149 |
+ if (ret < 0) |
|
150 |
+ return ret; |
|
151 |
+ wc->chunk_index++; |
|
152 |
+ ret = avio_open2(&oc->pb, oc->filename, AVIO_FLAG_WRITE, |
|
153 |
+ &s->interrupt_callback, NULL); |
|
154 |
+ if (ret < 0) |
|
155 |
+ return ret; |
|
156 |
+ oc->pb->seekable = 0; |
|
157 |
+ return 0; |
|
158 |
+} |
|
159 |
+ |
|
160 |
+static int chunk_end(AVFormatContext *s) |
|
161 |
+{ |
|
162 |
+ WebMChunkContext *wc = s->priv_data; |
|
163 |
+ AVFormatContext *oc = wc->avf; |
|
164 |
+ int ret; |
|
165 |
+ |
|
166 |
+ if (wc->chunk_start_index == wc->chunk_index) |
|
167 |
+ return 0; |
|
168 |
+ // Flush the cluster in WebM muxer. |
|
169 |
+ oc->oformat->write_packet(oc, NULL); |
|
170 |
+ ret = avio_close(oc->pb); |
|
171 |
+ if (ret < 0) |
|
172 |
+ return ret; |
|
173 |
+ wc->chunk_count++; |
|
174 |
+ return 0; |
|
175 |
+} |
|
176 |
+ |
|
177 |
+static int webm_chunk_write_packet(AVFormatContext *s, AVPacket *pkt) |
|
178 |
+{ |
|
179 |
+ WebMChunkContext *wc = s->priv_data; |
|
180 |
+ AVFormatContext *oc = wc->avf; |
|
181 |
+ AVStream *st = s->streams[pkt->stream_index]; |
|
182 |
+ int ret; |
|
183 |
+ |
|
184 |
+ if (st->codec->codec_type == AVMEDIA_TYPE_AUDIO) { |
|
185 |
+ wc->duration_written += av_rescale_q(pkt->pts - wc->prev_pts, |
|
186 |
+ st->time_base, |
|
187 |
+ (AVRational) {1, 1000}); |
|
188 |
+ wc->prev_pts = pkt->pts; |
|
189 |
+ } |
|
190 |
+ |
|
191 |
+ // For video, a new chunk is started only on key frames. For audio, a new |
|
192 |
+ // chunk is started based on chunk_duration. |
|
193 |
+ if ((st->codec->codec_type == AVMEDIA_TYPE_VIDEO && |
|
194 |
+ (pkt->flags & AV_PKT_FLAG_KEY)) || |
|
195 |
+ (st->codec->codec_type == AVMEDIA_TYPE_AUDIO && |
|
196 |
+ (pkt->pts == 0 || wc->duration_written >= wc->chunk_duration))) { |
|
197 |
+ wc->duration_written = 0; |
|
198 |
+ if ((ret = chunk_end(s)) < 0 || (ret = chunk_start(s)) < 0) { |
|
199 |
+ goto fail; |
|
200 |
+ } |
|
201 |
+ } |
|
202 |
+ |
|
203 |
+ if (st->codec->codec_type == AVMEDIA_TYPE_AUDIO && pkt->pts == 0) { |
|
204 |
+ goto fail; |
|
205 |
+ } |
|
206 |
+ ret = oc->oformat->write_packet(oc, pkt); |
|
207 |
+ if (ret < 0) |
|
208 |
+ goto fail; |
|
209 |
+ |
|
210 |
+fail: |
|
211 |
+ if (ret < 0) { |
|
212 |
+ oc->streams = NULL; |
|
213 |
+ oc->nb_streams = 0; |
|
214 |
+ avformat_free_context(oc); |
|
215 |
+ } |
|
216 |
+ |
|
217 |
+ return ret; |
|
218 |
+} |
|
219 |
+ |
|
220 |
+static int webm_chunk_write_trailer(AVFormatContext *s) |
|
221 |
+{ |
|
222 |
+ WebMChunkContext *wc = s->priv_data; |
|
223 |
+ AVFormatContext *oc = wc->avf; |
|
224 |
+ oc->oformat->write_trailer(oc); |
|
225 |
+ chunk_end(s); |
|
226 |
+ oc->streams = NULL; |
|
227 |
+ oc->nb_streams = 0; |
|
228 |
+ avformat_free_context(oc); |
|
229 |
+ return 0; |
|
230 |
+} |
|
231 |
+ |
|
232 |
+#define OFFSET(x) offsetof(WebMChunkContext, x) |
|
233 |
+static const AVOption options[] = { |
|
234 |
+ { "chunk_start_index", "start index of the chunk", OFFSET(chunk_start_index), AV_OPT_TYPE_INT, {.i64 = 0}, 0, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM }, |
|
235 |
+ { "header", "filename of the header where the initialization data will be written", OFFSET(header_filename), AV_OPT_TYPE_STRING, { 0 }, 0, 0, AV_OPT_FLAG_ENCODING_PARAM }, |
|
236 |
+ { "audio_chunk_duration", "duration of each chunk in milliseconds", OFFSET(chunk_duration), AV_OPT_TYPE_INT, {.i64 = 5000}, 0, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM }, |
|
237 |
+ { NULL }, |
|
238 |
+}; |
|
239 |
+ |
|
240 |
+#if CONFIG_WEBM_CHUNK_MUXER |
|
241 |
+static const AVClass webm_chunk_class = { |
|
242 |
+ .class_name = "WebM Chunk Muxer", |
|
243 |
+ .item_name = av_default_item_name, |
|
244 |
+ .option = options, |
|
245 |
+ .version = LIBAVUTIL_VERSION_INT, |
|
246 |
+}; |
|
247 |
+ |
|
248 |
+AVOutputFormat ff_webm_chunk_muxer = { |
|
249 |
+ .name = "webm_chunk", |
|
250 |
+ .long_name = NULL_IF_CONFIG_SMALL("WebM Chunk Muxer"), |
|
251 |
+ .mime_type = "video/webm", |
|
252 |
+ .extensions = "chk", |
|
253 |
+ .flags = AVFMT_NOFILE | AVFMT_GLOBALHEADER | AVFMT_NEEDNUMBER | |
|
254 |
+ AVFMT_TS_NONSTRICT | AVFMT_ALLOW_FLUSH, |
|
255 |
+ .priv_data_size = sizeof(WebMChunkContext), |
|
256 |
+ .write_header = webm_chunk_write_header, |
|
257 |
+ .write_packet = webm_chunk_write_packet, |
|
258 |
+ .write_trailer = webm_chunk_write_trailer, |
|
259 |
+ .priv_class = &webm_chunk_class, |
|
260 |
+}; |
|
261 |
+#endif |