Convert text WebVTT output to muxer.
Now text-based WebVTT also uses the generic media pipeline. This converts the WebVttTextOutputHandler to a WebVttMuxer to be more consistent with the other muxer types. This also allows choosing between single-segment text and multi-segment. Before, we would generate both and use single-segment for DASH and multi-segment for HLS; but now you can choose between either and either are supported in both DASH and HLS. Change-Id: I6f7edda09e01b5f40e819290d3fe6e88677018d9
This commit is contained in:
parent
2909ca0c77
commit
56908a83a7
|
@ -13,6 +13,7 @@
|
||||||
#include "packager/media/formats/mp4/mp4_muxer.h"
|
#include "packager/media/formats/mp4/mp4_muxer.h"
|
||||||
#include "packager/media/formats/packed_audio/packed_audio_writer.h"
|
#include "packager/media/formats/packed_audio/packed_audio_writer.h"
|
||||||
#include "packager/media/formats/webm/webm_muxer.h"
|
#include "packager/media/formats/webm/webm_muxer.h"
|
||||||
|
#include "packager/media/formats/webvtt/webvtt_muxer.h"
|
||||||
#include "packager/packager.h"
|
#include "packager/packager.h"
|
||||||
|
|
||||||
namespace shaka {
|
namespace shaka {
|
||||||
|
@ -20,9 +21,9 @@ namespace media {
|
||||||
|
|
||||||
MuxerFactory::MuxerFactory(const PackagingParams& packaging_params)
|
MuxerFactory::MuxerFactory(const PackagingParams& packaging_params)
|
||||||
: mp4_params_(packaging_params.mp4_output_params),
|
: mp4_params_(packaging_params.mp4_output_params),
|
||||||
|
temp_dir_(packaging_params.temp_dir),
|
||||||
transport_stream_timestamp_offset_ms_(
|
transport_stream_timestamp_offset_ms_(
|
||||||
packaging_params.transport_stream_timestamp_offset_ms),
|
packaging_params.transport_stream_timestamp_offset_ms) {}
|
||||||
temp_dir_(packaging_params.temp_dir) {}
|
|
||||||
|
|
||||||
std::shared_ptr<Muxer> MuxerFactory::CreateMuxer(
|
std::shared_ptr<Muxer> MuxerFactory::CreateMuxer(
|
||||||
MediaContainerName output_format,
|
MediaContainerName output_format,
|
||||||
|
@ -48,6 +49,9 @@ std::shared_ptr<Muxer> MuxerFactory::CreateMuxer(
|
||||||
case CONTAINER_WEBM:
|
case CONTAINER_WEBM:
|
||||||
muxer = std::make_shared<webm::WebMMuxer>(options);
|
muxer = std::make_shared<webm::WebMMuxer>(options);
|
||||||
break;
|
break;
|
||||||
|
case CONTAINER_WEBVTT:
|
||||||
|
muxer = std::make_shared<webvtt::WebVttMuxer>(options);
|
||||||
|
break;
|
||||||
case CONTAINER_MPEG2TS:
|
case CONTAINER_MPEG2TS:
|
||||||
muxer = std::make_shared<mp2t::TsMuxer>(options);
|
muxer = std::make_shared<mp2t::TsMuxer>(options);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -42,13 +42,17 @@ class MuxerFactory {
|
||||||
/// this will replace the clock for all muxers created after this call.
|
/// this will replace the clock for all muxers created after this call.
|
||||||
void OverrideClock(base::Clock* clock);
|
void OverrideClock(base::Clock* clock);
|
||||||
|
|
||||||
|
void SetTsStreamOffset(uint32_t offset_ms) {
|
||||||
|
transport_stream_timestamp_offset_ms_ = offset_ms;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
MuxerFactory(const MuxerFactory&) = delete;
|
MuxerFactory(const MuxerFactory&) = delete;
|
||||||
MuxerFactory& operator=(const MuxerFactory&) = delete;
|
MuxerFactory& operator=(const MuxerFactory&) = delete;
|
||||||
|
|
||||||
const Mp4OutputParams mp4_params_;
|
const Mp4OutputParams mp4_params_;
|
||||||
const uint32_t transport_stream_timestamp_offset_ms_ = 0;
|
|
||||||
const std::string temp_dir_;
|
const std::string temp_dir_;
|
||||||
|
uint32_t transport_stream_timestamp_offset_ms_ = 0;
|
||||||
base::Clock* clock_ = nullptr;
|
base::Clock* clock_ = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -833,6 +833,21 @@ class PackagerFunctionalTest(PackagerAppTest):
|
||||||
self.assertPackageSuccess(streams, flags)
|
self.assertPackageSuccess(streams, flags)
|
||||||
self._CheckTestResults('segmented-webvtt-with-language-override')
|
self._CheckTestResults('segmented-webvtt-with-language-override')
|
||||||
|
|
||||||
|
def testSegmentedWebVttText(self):
|
||||||
|
streams = self._GetStreams(
|
||||||
|
['text'], test_files=['bear-english.vtt'], segmented=True)
|
||||||
|
flags = self._GetFlags(output_hls=True, output_dash=True)
|
||||||
|
|
||||||
|
self.assertPackageSuccess(streams, flags)
|
||||||
|
self._CheckTestResults('segmented-webvtt-text')
|
||||||
|
|
||||||
|
def testSingleFileWebVttText(self):
|
||||||
|
streams = self._GetStreams(['text'], test_files=['bear-english.vtt'])
|
||||||
|
flags = self._GetFlags(output_hls=True, output_dash=True)
|
||||||
|
|
||||||
|
self.assertPackageSuccess(streams, flags)
|
||||||
|
self._CheckTestResults('single-file-webvtt-text')
|
||||||
|
|
||||||
def testMp4TrailingMoov(self):
|
def testMp4TrailingMoov(self):
|
||||||
self.assertPackageSuccess(
|
self.assertPackageSuccess(
|
||||||
self._GetStreams(['audio', 'video'],
|
self._GetStreams(['audio', 'video'],
|
||||||
|
@ -1930,7 +1945,8 @@ class PackagerCommandParsingTest(PackagerAppTest):
|
||||||
packaging_result = self.packager.Package(audio_video_stream + text_stream,
|
packaging_result = self.packager.Package(audio_video_stream + text_stream,
|
||||||
self._GetFlags())
|
self._GetFlags())
|
||||||
# Expect the test to fail but we do not expect a crash.
|
# Expect the test to fail but we do not expect a crash.
|
||||||
self.assertEqual(packaging_result, 1)
|
self.assertNotEqual(packaging_result, 0)
|
||||||
|
self.assertLess(packaging_result, 10)
|
||||||
|
|
||||||
def testInconsistentOutputAndSegmentTemplateFormat(self):
|
def testInconsistentOutputAndSegmentTemplateFormat(self):
|
||||||
test_file = os.path.join(self.test_data_dir, 'bear-640x360.mp4')
|
test_file = os.path.join(self.test_data_dir, 'bear-640x360.mp4')
|
||||||
|
|
|
@ -8,3 +8,4 @@ Yup, that's a bear, eh.
|
||||||
|
|
||||||
00:00:01.000 --> 00:00:04.700
|
00:00:01.000 --> 00:00:04.700
|
||||||
He 's... um... doing bear-like stuff.
|
He 's... um... doing bear-like stuff.
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<Period id="0">
|
<Period id="0">
|
||||||
<AdaptationSet id="0" contentType="text" subsegmentAlignment="true">
|
<AdaptationSet id="0" contentType="text" subsegmentAlignment="true">
|
||||||
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="caption"/>
|
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="caption"/>
|
||||||
<Representation id="0" bandwidth="256" mimeType="text/vtt">
|
<Representation id="0" bandwidth="273" mimeType="text/vtt">
|
||||||
<BaseURL>bear-english-text.vtt</BaseURL>
|
<BaseURL>bear-english-text.vtt</BaseURL>
|
||||||
</Representation>
|
</Representation>
|
||||||
</AdaptationSet>
|
</AdaptationSet>
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
|
|
||||||
#EXT-X-INDEPENDENT-SEGMENTS
|
#EXT-X-INDEPENDENT-SEGMENTS
|
||||||
|
|
||||||
#EXT-X-MEDIA:TYPE=AUDIO,URI="bear-640x360-audio.m3u8",GROUP-ID="default-audio-group",NAME="stream_1",AUTOSELECT=YES,CHANNELS="2"
|
#EXT-X-MEDIA:TYPE=AUDIO,URI="bear-640x360-audio.m3u8",GROUP-ID="default-audio-group",NAME="stream_0",AUTOSELECT=YES,CHANNELS="2"
|
||||||
|
|
||||||
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="bear-english-text.m3u8",GROUP-ID="default-text-group",NAME="stream_0",AUTOSELECT=YES
|
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="bear-english-text.m3u8",GROUP-ID="default-text-group",NAME="stream_2",AUTOSELECT=YES
|
||||||
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1108115,AVERAGE-BANDWIDTH=1006069,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=29.970,AUDIO="default-audio-group",SUBTITLES="default-text-group"
|
#EXT-X-STREAM-INF:BANDWIDTH=1108115,AVERAGE-BANDWIDTH=1006069,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=29.970,AUDIO="default-audio-group",SUBTITLES="default-text-group"
|
||||||
bear-640x360-video.m3u8
|
bear-640x360-video.m3u8
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
|
|
||||||
#EXT-X-INDEPENDENT-SEGMENTS
|
#EXT-X-INDEPENDENT-SEGMENTS
|
||||||
|
|
||||||
#EXT-X-MEDIA:TYPE=AUDIO,URI="stream_1.m3u8",GROUP-ID="default-audio-group",NAME="stream_1",AUTOSELECT=YES,CHANNELS="2"
|
#EXT-X-MEDIA:TYPE=AUDIO,URI="stream_0.m3u8",GROUP-ID="default-audio-group",NAME="stream_0",AUTOSELECT=YES,CHANNELS="2"
|
||||||
|
|
||||||
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="stream_0.m3u8",GROUP-ID="default-text-group",NAME="stream_0",AUTOSELECT=YES
|
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="stream_3.m3u8",GROUP-ID="default-text-group",NAME="stream_3",AUTOSELECT=YES
|
||||||
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1108115,AVERAGE-BANDWIDTH=1006069,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=29.970,AUDIO="default-audio-group",SUBTITLES="default-text-group"
|
#EXT-X-STREAM-INF:BANDWIDTH=1108115,AVERAGE-BANDWIDTH=1006069,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=29.970,AUDIO="default-audio-group",SUBTITLES="default-text-group"
|
||||||
stream_2.m3u8
|
stream_1.m3u8
|
||||||
|
|
|
@ -3,14 +3,11 @@
|
||||||
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
||||||
#EXT-X-TARGETDURATION:2
|
#EXT-X-TARGETDURATION:2
|
||||||
#EXT-X-PLAYLIST-TYPE:VOD
|
#EXT-X-PLAYLIST-TYPE:VOD
|
||||||
#EXTINF:1.000,
|
#EXT-X-MAP:URI="bear-640x360-audio-init.mp4"
|
||||||
bear-english-text-1.vtt
|
#EXTINF:1.022,
|
||||||
#EXTINF:1.000,
|
bear-640x360-audio-1.m4s
|
||||||
bear-english-text-2.vtt
|
#EXTINF:0.998,
|
||||||
#EXTINF:1.000,
|
bear-640x360-audio-2.m4s
|
||||||
bear-english-text-3.vtt
|
#EXTINF:0.720,
|
||||||
#EXTINF:1.000,
|
bear-640x360-audio-3.m4s
|
||||||
bear-english-text-4.vtt
|
|
||||||
#EXTINF:1.000,
|
|
||||||
bear-english-text-5.vtt
|
|
||||||
#EXT-X-ENDLIST
|
#EXT-X-ENDLIST
|
||||||
|
|
|
@ -3,11 +3,11 @@
|
||||||
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
||||||
#EXT-X-TARGETDURATION:2
|
#EXT-X-TARGETDURATION:2
|
||||||
#EXT-X-PLAYLIST-TYPE:VOD
|
#EXT-X-PLAYLIST-TYPE:VOD
|
||||||
#EXT-X-MAP:URI="bear-640x360-audio-init.mp4"
|
#EXT-X-MAP:URI="bear-640x360-video-init.mp4"
|
||||||
#EXTINF:1.022,
|
#EXTINF:1.001,
|
||||||
bear-640x360-audio-1.m4s
|
bear-640x360-video-1.m4s
|
||||||
#EXTINF:0.998,
|
#EXTINF:1.001,
|
||||||
bear-640x360-audio-2.m4s
|
bear-640x360-video-2.m4s
|
||||||
#EXTINF:0.720,
|
#EXTINF:0.734,
|
||||||
bear-640x360-audio-3.m4s
|
bear-640x360-video-3.m4s
|
||||||
#EXT-X-ENDLIST
|
#EXT-X-ENDLIST
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
#EXTM3U
|
|
||||||
#EXT-X-VERSION:6
|
|
||||||
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
|
||||||
#EXT-X-TARGETDURATION:2
|
|
||||||
#EXT-X-PLAYLIST-TYPE:VOD
|
|
||||||
#EXT-X-MAP:URI="bear-640x360-video-init.mp4"
|
|
||||||
#EXTINF:1.001,
|
|
||||||
bear-640x360-video-1.m4s
|
|
||||||
#EXTINF:1.001,
|
|
||||||
bear-640x360-video-2.m4s
|
|
||||||
#EXTINF:0.734,
|
|
||||||
bear-640x360-video-3.m4s
|
|
||||||
#EXT-X-ENDLIST
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXT-X-VERSION:6
|
||||||
|
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
||||||
|
#EXT-X-TARGETDURATION:2
|
||||||
|
#EXT-X-PLAYLIST-TYPE:VOD
|
||||||
|
#EXTINF:1.000,
|
||||||
|
bear-english-text-1.vtt
|
||||||
|
#EXTINF:1.000,
|
||||||
|
bear-english-text-2.vtt
|
||||||
|
#EXTINF:1.000,
|
||||||
|
bear-english-text-3.vtt
|
||||||
|
#EXTINF:1.000,
|
||||||
|
bear-english-text-4.vtt
|
||||||
|
#EXTINF:1.000,
|
||||||
|
bear-english-text-5.vtt
|
||||||
|
#EXT-X-ENDLIST
|
|
@ -3,9 +3,9 @@
|
||||||
|
|
||||||
#EXT-X-INDEPENDENT-SEGMENTS
|
#EXT-X-INDEPENDENT-SEGMENTS
|
||||||
|
|
||||||
#EXT-X-MEDIA:TYPE=AUDIO,URI="stream_1.m3u8",GROUP-ID="default-audio-group",NAME="stream_1",AUTOSELECT=YES,CHANNELS="2"
|
#EXT-X-MEDIA:TYPE=AUDIO,URI="stream_0.m3u8",GROUP-ID="default-audio-group",NAME="stream_0",AUTOSELECT=YES,CHANNELS="2"
|
||||||
|
|
||||||
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="stream_0.m3u8",GROUP-ID="default-text-group",NAME="stream_0",AUTOSELECT=YES,CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,private.accessibility.widevine-special"
|
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="stream_2.m3u8",GROUP-ID="default-text-group",NAME="stream_2",AUTOSELECT=YES,CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,private.accessibility.widevine-special"
|
||||||
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1217518,AVERAGE-BANDWIDTH=1117319,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=29.970,AUDIO="default-audio-group",SUBTITLES="default-text-group"
|
#EXT-X-STREAM-INF:BANDWIDTH=1217518,AVERAGE-BANDWIDTH=1117319,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=29.970,AUDIO="default-audio-group",SUBTITLES="default-text-group"
|
||||||
stream_2.m3u8
|
stream_1.m3u8
|
||||||
|
|
|
@ -3,14 +3,10 @@
|
||||||
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
||||||
#EXT-X-TARGETDURATION:2
|
#EXT-X-TARGETDURATION:2
|
||||||
#EXT-X-PLAYLIST-TYPE:VOD
|
#EXT-X-PLAYLIST-TYPE:VOD
|
||||||
#EXTINF:1.000,
|
#EXTINF:1.045,
|
||||||
bear-english-text-1.vtt
|
bear-640x360-audio-1.ts
|
||||||
#EXTINF:1.000,
|
#EXTINF:0.998,
|
||||||
bear-english-text-2.vtt
|
bear-640x360-audio-2.ts
|
||||||
#EXTINF:1.000,
|
#EXTINF:0.720,
|
||||||
bear-english-text-3.vtt
|
bear-640x360-audio-3.ts
|
||||||
#EXTINF:1.000,
|
|
||||||
bear-english-text-4.vtt
|
|
||||||
#EXTINF:1.000,
|
|
||||||
bear-english-text-5.vtt
|
|
||||||
#EXT-X-ENDLIST
|
#EXT-X-ENDLIST
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
||||||
#EXT-X-TARGETDURATION:2
|
#EXT-X-TARGETDURATION:2
|
||||||
#EXT-X-PLAYLIST-TYPE:VOD
|
#EXT-X-PLAYLIST-TYPE:VOD
|
||||||
#EXTINF:1.045,
|
#EXTINF:1.001,
|
||||||
bear-640x360-audio-1.ts
|
bear-640x360-video-1.ts
|
||||||
#EXTINF:0.998,
|
#EXTINF:1.001,
|
||||||
bear-640x360-audio-2.ts
|
bear-640x360-video-2.ts
|
||||||
#EXTINF:0.720,
|
#EXTINF:0.734,
|
||||||
bear-640x360-audio-3.ts
|
bear-640x360-video-3.ts
|
||||||
#EXT-X-ENDLIST
|
#EXT-X-ENDLIST
|
||||||
|
|
|
@ -3,10 +3,14 @@
|
||||||
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
||||||
#EXT-X-TARGETDURATION:2
|
#EXT-X-TARGETDURATION:2
|
||||||
#EXT-X-PLAYLIST-TYPE:VOD
|
#EXT-X-PLAYLIST-TYPE:VOD
|
||||||
#EXTINF:1.001,
|
#EXTINF:1.000,
|
||||||
bear-640x360-video-1.ts
|
bear-english-text-1.vtt
|
||||||
#EXTINF:1.001,
|
#EXTINF:1.000,
|
||||||
bear-640x360-video-2.ts
|
bear-english-text-2.vtt
|
||||||
#EXTINF:0.734,
|
#EXTINF:1.000,
|
||||||
bear-640x360-video-3.ts
|
bear-english-text-3.vtt
|
||||||
|
#EXTINF:1.000,
|
||||||
|
bear-english-text-4.vtt
|
||||||
|
#EXTINF:1.000,
|
||||||
|
bear-english-text-5.vtt
|
||||||
#EXT-X-ENDLIST
|
#EXT-X-ENDLIST
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
WEBVTT
|
||||||
|
X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:9000
|
||||||
|
|
||||||
|
STYLE
|
||||||
|
::cue { color:lime }
|
||||||
|
|
||||||
|
00:00:00.000 --> 00:00:00.800
|
||||||
|
Yup, that's a bear, eh.
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
WEBVTT
|
||||||
|
X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:9000
|
||||||
|
|
||||||
|
STYLE
|
||||||
|
::cue { color:lime }
|
||||||
|
|
||||||
|
00:00:01.000 --> 00:00:04.700
|
||||||
|
He 's... um... doing bear-like stuff.
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
WEBVTT
|
||||||
|
X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:9000
|
||||||
|
|
||||||
|
STYLE
|
||||||
|
::cue { color:lime }
|
||||||
|
|
||||||
|
00:00:01.000 --> 00:00:04.700
|
||||||
|
He 's... um... doing bear-like stuff.
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
WEBVTT
|
||||||
|
X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:9000
|
||||||
|
|
||||||
|
STYLE
|
||||||
|
::cue { color:lime }
|
||||||
|
|
||||||
|
00:00:01.000 --> 00:00:04.700
|
||||||
|
He 's... um... doing bear-like stuff.
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
WEBVTT
|
||||||
|
X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:9000
|
||||||
|
|
||||||
|
STYLE
|
||||||
|
::cue { color:lime }
|
||||||
|
|
||||||
|
00:00:01.000 --> 00:00:04.700
|
||||||
|
He 's... um... doing bear-like stuff.
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
#EXTM3U
|
||||||
|
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
||||||
|
|
||||||
|
#EXT-X-INDEPENDENT-SEGMENTS
|
||||||
|
|
||||||
|
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="stream_0.m3u8",GROUP-ID="default-text-group",NAME="stream_0",AUTOSELECT=YES
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>-->
|
||||||
|
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd" profiles="urn:mpeg:dash:profile:isoff-live:2011" minBufferTime="PT2S" type="dynamic" publishTime="some_time" availabilityStartTime="some_time" minimumUpdatePeriod="PT5S" timeShiftBufferDepth="PT1800S">
|
||||||
|
<Period id="0" start="PT0S">
|
||||||
|
<AdaptationSet id="0" contentType="text" segmentAlignment="true">
|
||||||
|
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="subtitle"/>
|
||||||
|
<Representation id="0" bandwidth="1216" mimeType="text/vtt">
|
||||||
|
<SegmentTemplate timescale="1000" media="bear-english-text-$Number$.vtt" startNumber="1">
|
||||||
|
<SegmentTimeline>
|
||||||
|
<S t="0" d="1000" r="4"/>
|
||||||
|
</SegmentTimeline>
|
||||||
|
</SegmentTemplate>
|
||||||
|
</Representation>
|
||||||
|
</AdaptationSet>
|
||||||
|
</Period>
|
||||||
|
</MPD>
|
|
@ -3,4 +3,4 @@
|
||||||
|
|
||||||
#EXT-X-INDEPENDENT-SEGMENTS
|
#EXT-X-INDEPENDENT-SEGMENTS
|
||||||
|
|
||||||
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="stream_0.m3u8",GROUP-ID="default-text-group",LANGUAGE="pt",NAME="stream_0",AUTOSELECT=YES
|
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="stream_1.m3u8",GROUP-ID="default-text-group",LANGUAGE="pt",NAME="stream_1",AUTOSELECT=YES
|
||||||
|
|
16
packager/app/test/testdata/segmented-webvtt-with-language-override/stream_1.m3u8
vendored
Normal file
16
packager/app/test/testdata/segmented-webvtt-with-language-override/stream_1.m3u8
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXT-X-VERSION:6
|
||||||
|
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
||||||
|
#EXT-X-TARGETDURATION:1
|
||||||
|
#EXT-X-PLAYLIST-TYPE:VOD
|
||||||
|
#EXTINF:1.000,
|
||||||
|
bear-english-text-1.vtt
|
||||||
|
#EXTINF:1.000,
|
||||||
|
bear-english-text-2.vtt
|
||||||
|
#EXTINF:1.000,
|
||||||
|
bear-english-text-3.vtt
|
||||||
|
#EXTINF:1.000,
|
||||||
|
bear-english-text-4.vtt
|
||||||
|
#EXTINF:1.000,
|
||||||
|
bear-english-text-5.vtt
|
||||||
|
#EXT-X-ENDLIST
|
|
@ -0,0 +1,12 @@
|
||||||
|
WEBVTT
|
||||||
|
X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:9000
|
||||||
|
|
||||||
|
STYLE
|
||||||
|
::cue { color:lime }
|
||||||
|
|
||||||
|
00:00:00.000 --> 00:00:00.800
|
||||||
|
Yup, that's a bear, eh.
|
||||||
|
|
||||||
|
00:00:01.000 --> 00:00:04.700
|
||||||
|
He 's... um... doing bear-like stuff.
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
#EXTM3U
|
||||||
|
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
||||||
|
|
||||||
|
#EXT-X-INDEPENDENT-SEGMENTS
|
||||||
|
|
||||||
|
#EXT-X-MEDIA:TYPE=SUBTITLES,URI="stream_0.m3u8",GROUP-ID="default-text-group",NAME="stream_0",AUTOSELECT=YES
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>-->
|
||||||
|
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" minBufferTime="PT2S" type="static" mediaPresentationDuration="PT4.699999809265137S">
|
||||||
|
<Period id="0">
|
||||||
|
<AdaptationSet id="0" contentType="text" subsegmentAlignment="true">
|
||||||
|
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="subtitle"/>
|
||||||
|
<Representation id="0" bandwidth="353" mimeType="text/vtt">
|
||||||
|
<BaseURL>bear-english-text.vtt</BaseURL>
|
||||||
|
</Representation>
|
||||||
|
</AdaptationSet>
|
||||||
|
</Period>
|
||||||
|
</MPD>
|
|
@ -0,0 +1,8 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXT-X-VERSION:6
|
||||||
|
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
|
||||||
|
#EXT-X-TARGETDURATION:5
|
||||||
|
#EXT-X-PLAYLIST-TYPE:VOD
|
||||||
|
#EXTINF:4.700,
|
||||||
|
bear-english-text.vtt
|
||||||
|
#EXT-X-ENDLIST
|
|
@ -8,3 +8,4 @@ Yup, that's a bear, eh.
|
||||||
|
|
||||||
00:00:01.000 --> 00:00:04.700
|
00:00:01.000 --> 00:00:04.700
|
||||||
He 's... um... doing bear-like stuff.
|
He 's... um... doing bear-like stuff.
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" minBufferTime="PT2S" type="static" mediaPresentationDuration="PT2.7360665798187256S">
|
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" minBufferTime="PT2S" type="static" mediaPresentationDuration="PT2.7360665798187256S">
|
||||||
<Period id="0">
|
<Period id="0">
|
||||||
<AdaptationSet id="0" contentType="text" subsegmentAlignment="true">
|
<AdaptationSet id="0" contentType="text" subsegmentAlignment="true">
|
||||||
<Representation id="0" bandwidth="256" mimeType="text/vtt">
|
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="subtitle"/>
|
||||||
|
<Representation id="0" bandwidth="273" mimeType="text/vtt">
|
||||||
<BaseURL>bear-english-text.vtt</BaseURL>
|
<BaseURL>bear-english-text.vtt</BaseURL>
|
||||||
</Representation>
|
</Representation>
|
||||||
</AdaptationSet>
|
</AdaptationSet>
|
||||||
|
|
|
@ -391,7 +391,8 @@ bool MediaPlaylist::SetMediaInfo(const MediaInfo& media_info) {
|
||||||
time_scale_ = time_scale;
|
time_scale_ = time_scale;
|
||||||
media_info_ = media_info;
|
media_info_ = media_info;
|
||||||
language_ = GetLanguage(media_info);
|
language_ = GetLanguage(media_info);
|
||||||
use_byte_range_ = !media_info_.has_segment_template_url();
|
use_byte_range_ = !media_info_.has_segment_template_url() &&
|
||||||
|
media_info_.container_type() != MediaInfo::CONTAINER_TEXT;
|
||||||
characteristics_ =
|
characteristics_ =
|
||||||
std::vector<std::string>(media_info_.hls_characteristics().begin(),
|
std::vector<std::string>(media_info_.hls_characteristics().begin(),
|
||||||
media_info_.hls_characteristics().end());
|
media_info_.hls_characteristics().end());
|
||||||
|
|
|
@ -68,7 +68,11 @@ Status Muxer::Process(std::unique_ptr<StreamData> stream_data) {
|
||||||
return FinalizeSegment(stream_data->stream_index, segment_info);
|
return FinalizeSegment(stream_data->stream_index, segment_info);
|
||||||
}
|
}
|
||||||
case StreamDataType::kMediaSample:
|
case StreamDataType::kMediaSample:
|
||||||
return AddSample(stream_data->stream_index, *stream_data->media_sample);
|
return AddMediaSample(stream_data->stream_index,
|
||||||
|
*stream_data->media_sample);
|
||||||
|
case StreamDataType::kTextSample:
|
||||||
|
return AddTextSample(stream_data->stream_index,
|
||||||
|
*stream_data->text_sample);
|
||||||
case StreamDataType::kCueEvent:
|
case StreamDataType::kCueEvent:
|
||||||
if (muxer_listener_) {
|
if (muxer_listener_) {
|
||||||
const int64_t time_scale =
|
const int64_t time_scale =
|
||||||
|
@ -99,6 +103,14 @@ Status Muxer::OnFlushRequest(size_t input_stream_index) {
|
||||||
return Finalize();
|
return Finalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Status Muxer::AddMediaSample(size_t stream_id, const MediaSample& sample) {
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status Muxer::AddTextSample(size_t stream_id, const TextSample& sample) {
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
Status Muxer::ReinitializeMuxer(int64_t timestamp) {
|
Status Muxer::ReinitializeMuxer(int64_t timestamp) {
|
||||||
if (muxer_listener_ && streams_.back()->is_encrypted()) {
|
if (muxer_listener_ && streams_.back()->is_encrypted()) {
|
||||||
const EncryptionConfig& encryption_config =
|
const EncryptionConfig& encryption_config =
|
||||||
|
|
|
@ -83,10 +83,13 @@ class Muxer : public MediaHandler {
|
||||||
// Final clean up.
|
// Final clean up.
|
||||||
virtual Status Finalize() = 0;
|
virtual Status Finalize() = 0;
|
||||||
|
|
||||||
// Add a new sample.
|
// Add a new media sample. This does nothing by default; so subclasses that
|
||||||
virtual Status AddSample(
|
// handle media samples will need to replace this.
|
||||||
size_t stream_id,
|
virtual Status AddMediaSample(size_t stream_id, const MediaSample& sample);
|
||||||
const MediaSample& sample) = 0;
|
|
||||||
|
// Add a new text sample. This does nothing by default; so subclasses that
|
||||||
|
// handle text samples will need to replace this.
|
||||||
|
virtual Status AddTextSample(size_t stream_id, const TextSample& sample);
|
||||||
|
|
||||||
// Finalize the segment or subsegment.
|
// Finalize the segment or subsegment.
|
||||||
virtual Status FinalizeSegment(
|
virtual Status FinalizeSegment(
|
||||||
|
|
|
@ -73,8 +73,14 @@ void MpdNotifyMuxerListener::OnMediaStart(
|
||||||
}
|
}
|
||||||
for (const std::string& accessibility : accessibilities_)
|
for (const std::string& accessibility : accessibilities_)
|
||||||
media_info->add_dash_accessibilities(accessibility);
|
media_info->add_dash_accessibilities(accessibility);
|
||||||
for (const std::string& role : roles_)
|
if (roles_.empty() && stream_info.stream_type() == kStreamText) {
|
||||||
media_info->add_dash_roles(role);
|
// If there aren't any roles, default to "subtitle" since some apps may
|
||||||
|
// require it to distinguish between subtitle/caption.
|
||||||
|
media_info->add_dash_roles("subtitle");
|
||||||
|
} else {
|
||||||
|
for (const std::string& role : roles_)
|
||||||
|
media_info->add_dash_roles(role);
|
||||||
|
}
|
||||||
|
|
||||||
if (is_encrypted_) {
|
if (is_encrypted_) {
|
||||||
internal::SetContentProtectionFields(protection_scheme_, default_key_id_,
|
internal::SetContentProtectionFields(protection_scheme_, default_key_id_,
|
||||||
|
|
|
@ -166,9 +166,8 @@ void AddAudioInfo(const AudioStreamInfo* audio_stream_info,
|
||||||
|
|
||||||
void AddTextInfo(const TextStreamInfo& text_stream_info,
|
void AddTextInfo(const TextStreamInfo& text_stream_info,
|
||||||
MediaInfo* media_info) {
|
MediaInfo* media_info) {
|
||||||
// For now, set everything as subtitle.
|
// TODO(modmaker): Set kind.
|
||||||
MediaInfo::TextInfo* text_info = media_info->mutable_text_info();
|
MediaInfo::TextInfo* text_info = media_info->mutable_text_info();
|
||||||
text_info->set_type(MediaInfo::TextInfo::SUBTITLE);
|
|
||||||
text_info->set_codec(text_stream_info.codec_string());
|
text_info->set_codec(text_stream_info.codec_string());
|
||||||
text_info->set_language(text_stream_info.language());
|
text_info->set_language(text_stream_info.language());
|
||||||
}
|
}
|
||||||
|
@ -234,8 +233,9 @@ bool GenerateMediaInfo(const MuxerOptions& muxer_options,
|
||||||
|
|
||||||
SetMediaInfoMuxerOptions(muxer_options, media_info);
|
SetMediaInfoMuxerOptions(muxer_options, media_info);
|
||||||
SetMediaInfoStreamInfo(stream_info, media_info);
|
SetMediaInfoStreamInfo(stream_info, media_info);
|
||||||
media_info->set_reference_time_scale(reference_time_scale);
|
|
||||||
SetMediaInfoContainerType(container_type, media_info);
|
SetMediaInfoContainerType(container_type, media_info);
|
||||||
|
if (reference_time_scale > 0)
|
||||||
|
media_info->set_reference_time_scale(reference_time_scale);
|
||||||
if (muxer_options.bandwidth > 0)
|
if (muxer_options.bandwidth > 0)
|
||||||
media_info->set_bandwidth(muxer_options.bandwidth);
|
media_info->set_bandwidth(muxer_options.bandwidth);
|
||||||
|
|
||||||
|
|
|
@ -32,15 +32,15 @@ Status TsMuxer::Finalize() {
|
||||||
return segmenter_->Finalize();
|
return segmenter_->Finalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
Status TsMuxer::AddSample(size_t stream_id, const MediaSample& sample) {
|
Status TsMuxer::AddMediaSample(size_t stream_id, const MediaSample& sample) {
|
||||||
DCHECK_EQ(stream_id, 0u);
|
DCHECK_EQ(stream_id, 0u);
|
||||||
if (num_samples_ < 2) {
|
if (num_samples_ < 2) {
|
||||||
sample_durations_[num_samples_] = sample.duration() * kTsTimescale /
|
sample_durations_[num_samples_] =
|
||||||
streams().front()->time_scale();
|
sample.duration() * kTsTimescale / streams().front()->time_scale();
|
||||||
if (num_samples_ == 1 && muxer_listener())
|
if (num_samples_ == 1 && muxer_listener())
|
||||||
muxer_listener()->OnSampleDurationReady(sample_durations_[num_samples_]);
|
muxer_listener()->OnSampleDurationReady(sample_durations_[num_samples_]);
|
||||||
num_samples_++;
|
num_samples_++;
|
||||||
}
|
}
|
||||||
return segmenter_->AddSample(sample);
|
return segmenter_->AddSample(sample);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,8 +26,7 @@ class TsMuxer : public Muxer {
|
||||||
// Muxer implementation.
|
// Muxer implementation.
|
||||||
Status InitializeMuxer() override;
|
Status InitializeMuxer() override;
|
||||||
Status Finalize() override;
|
Status Finalize() override;
|
||||||
Status AddSample(size_t stream_id,
|
Status AddMediaSample(size_t stream_id, const MediaSample& sample) override;
|
||||||
const MediaSample& sample) override;
|
|
||||||
Status FinalizeSegment(size_t stream_id,
|
Status FinalizeSegment(size_t stream_id,
|
||||||
const SegmentInfo& sample) override;
|
const SegmentInfo& sample) override;
|
||||||
|
|
||||||
|
|
|
@ -181,7 +181,7 @@ Status MP4Muxer::Finalize() {
|
||||||
return Status::OK;
|
return Status::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
Status MP4Muxer::AddSample(size_t stream_id, const MediaSample& sample) {
|
Status MP4Muxer::AddMediaSample(size_t stream_id, const MediaSample& sample) {
|
||||||
if (to_be_initialized_) {
|
if (to_be_initialized_) {
|
||||||
RETURN_IF_ERROR(UpdateEditListOffsetFromSample(sample));
|
RETURN_IF_ERROR(UpdateEditListOffsetFromSample(sample));
|
||||||
RETURN_IF_ERROR(DelayInitializeMuxer());
|
RETURN_IF_ERROR(DelayInitializeMuxer());
|
||||||
|
|
|
@ -39,7 +39,7 @@ class MP4Muxer : public Muxer {
|
||||||
// Muxer implementation overrides.
|
// Muxer implementation overrides.
|
||||||
Status InitializeMuxer() override;
|
Status InitializeMuxer() override;
|
||||||
Status Finalize() override;
|
Status Finalize() override;
|
||||||
Status AddSample(size_t stream_id, const MediaSample& sample) override;
|
Status AddMediaSample(size_t stream_id, const MediaSample& sample) override;
|
||||||
Status FinalizeSegment(size_t stream_id,
|
Status FinalizeSegment(size_t stream_id,
|
||||||
const SegmentInfo& segment_info) override;
|
const SegmentInfo& segment_info) override;
|
||||||
|
|
||||||
|
|
|
@ -58,8 +58,8 @@ Status PackedAudioWriter::Finalize() {
|
||||||
return Status::OK;
|
return Status::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
Status PackedAudioWriter::AddSample(size_t stream_id,
|
Status PackedAudioWriter::AddMediaSample(size_t stream_id,
|
||||||
const MediaSample& sample) {
|
const MediaSample& sample) {
|
||||||
DCHECK_EQ(stream_id, 0u);
|
DCHECK_EQ(stream_id, 0u);
|
||||||
return segmenter_->AddSample(sample);
|
return segmenter_->AddSample(sample);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ class PackedAudioWriter : public Muxer {
|
||||||
// Muxer implementations.
|
// Muxer implementations.
|
||||||
Status InitializeMuxer() override;
|
Status InitializeMuxer() override;
|
||||||
Status Finalize() override;
|
Status Finalize() override;
|
||||||
Status AddSample(size_t stream_id, const MediaSample& sample) override;
|
Status AddMediaSample(size_t stream_id, const MediaSample& sample) override;
|
||||||
Status FinalizeSegment(size_t stream_id, const SegmentInfo& sample) override;
|
Status FinalizeSegment(size_t stream_id, const SegmentInfo& sample) override;
|
||||||
|
|
||||||
Status WriteSegment(const std::string& segment_path,
|
Status WriteSegment(const std::string& segment_path,
|
||||||
|
|
|
@ -58,7 +58,7 @@ Status WebMMuxer::Finalize() {
|
||||||
return Status::OK;
|
return Status::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
Status WebMMuxer::AddSample(size_t stream_id, const MediaSample& sample) {
|
Status WebMMuxer::AddMediaSample(size_t stream_id, const MediaSample& sample) {
|
||||||
DCHECK(segmenter_);
|
DCHECK(segmenter_);
|
||||||
DCHECK_EQ(stream_id, 0u);
|
DCHECK_EQ(stream_id, 0u);
|
||||||
if (sample.pts() < 0) {
|
if (sample.pts() < 0) {
|
||||||
|
|
|
@ -26,7 +26,7 @@ class WebMMuxer : public Muxer {
|
||||||
// Muxer implementation overrides.
|
// Muxer implementation overrides.
|
||||||
Status InitializeMuxer() override;
|
Status InitializeMuxer() override;
|
||||||
Status Finalize() override;
|
Status Finalize() override;
|
||||||
Status AddSample(size_t stream_id, const MediaSample& sample) override;
|
Status AddMediaSample(size_t stream_id, const MediaSample& sample) override;
|
||||||
Status FinalizeSegment(size_t stream_id,
|
Status FinalizeSegment(size_t stream_id,
|
||||||
const SegmentInfo& segment_info) override;
|
const SegmentInfo& segment_info) override;
|
||||||
|
|
||||||
|
|
|
@ -19,10 +19,10 @@
|
||||||
'text_readers.h',
|
'text_readers.h',
|
||||||
'webvtt_file_buffer.cc',
|
'webvtt_file_buffer.cc',
|
||||||
'webvtt_file_buffer.h',
|
'webvtt_file_buffer.h',
|
||||||
|
'webvtt_muxer.cc',
|
||||||
|
'webvtt_muxer.h',
|
||||||
'webvtt_parser.cc',
|
'webvtt_parser.cc',
|
||||||
'webvtt_parser.h',
|
'webvtt_parser.h',
|
||||||
'webvtt_text_output_handler.cc',
|
|
||||||
'webvtt_text_output_handler.h',
|
|
||||||
'webvtt_timestamp.cc',
|
'webvtt_timestamp.cc',
|
||||||
'webvtt_timestamp.h',
|
'webvtt_timestamp.h',
|
||||||
'webvtt_to_mp4_handler.cc',
|
'webvtt_to_mp4_handler.cc',
|
||||||
|
@ -40,8 +40,8 @@
|
||||||
'type': '<(gtest_target_type)',
|
'type': '<(gtest_target_type)',
|
||||||
'sources': [
|
'sources': [
|
||||||
'text_readers_unittest.cc',
|
'text_readers_unittest.cc',
|
||||||
|
'webvtt_muxer_unittest.cc',
|
||||||
'webvtt_parser_unittest.cc',
|
'webvtt_parser_unittest.cc',
|
||||||
'webvtt_text_output_handler_unittest.cc',
|
|
||||||
'webvtt_timestamp_unittest.cc',
|
'webvtt_timestamp_unittest.cc',
|
||||||
'webvtt_to_mp4_handler_unittest.cc',
|
'webvtt_to_mp4_handler_unittest.cc',
|
||||||
],
|
],
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
// Copyright 2020 Google LLC. All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file or at
|
||||||
|
// https://developers.google.com/open-source/licenses/bsd
|
||||||
|
|
||||||
|
#include "packager/media/formats/webvtt/webvtt_muxer.h"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "packager/file/file.h"
|
||||||
|
#include "packager/file/file_closer.h"
|
||||||
|
#include "packager/media/base/muxer_util.h"
|
||||||
|
#include "packager/media/formats/webvtt/webvtt_timestamp.h"
|
||||||
|
#include "packager/status_macros.h"
|
||||||
|
|
||||||
|
namespace shaka {
|
||||||
|
namespace media {
|
||||||
|
namespace webvtt {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string ToString(const std::vector<uint8_t>& v) {
|
||||||
|
return std::string(v.begin(), v.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
WebVttMuxer::WebVttMuxer(const MuxerOptions& options) : Muxer(options) {}
|
||||||
|
WebVttMuxer::~WebVttMuxer() {}
|
||||||
|
|
||||||
|
Status WebVttMuxer::InitializeMuxer() {
|
||||||
|
if (streams().size() != 1) {
|
||||||
|
return Status(error::MUXER_FAILURE, "Incorrect number of streams");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only initialize the stream once we see a cue to avoid empty files.
|
||||||
|
muxer_listener()->OnMediaStart(options(), *streams()[0],
|
||||||
|
streams()[0]->time_scale(),
|
||||||
|
MuxerListener::kContainerText);
|
||||||
|
|
||||||
|
buffer_.reset(
|
||||||
|
new WebVttFileBuffer(options().transport_stream_timestamp_offset_ms,
|
||||||
|
ToString(streams()[0]->codec_config())));
|
||||||
|
last_cue_ms_ = 0;
|
||||||
|
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status WebVttMuxer::Finalize() {
|
||||||
|
const float duration_ms = static_cast<float>(total_duration_ms_);
|
||||||
|
float duration_seconds = duration_ms / 1000;
|
||||||
|
|
||||||
|
// If we haven't seen any segments, this is a single-file. In this case,
|
||||||
|
// flush the single segment.
|
||||||
|
MuxerListener::MediaRanges ranges;
|
||||||
|
if (duration_seconds == 0 && last_cue_ms_ != 0) {
|
||||||
|
DCHECK(options().segment_template.empty());
|
||||||
|
duration_seconds = static_cast<float>(last_cue_ms_) / 1000;
|
||||||
|
|
||||||
|
uint64_t size;
|
||||||
|
RETURN_IF_ERROR(WriteToFile(options().output_file_name, &size));
|
||||||
|
// Insert a dummy value so the HLS generator will generate a segment list.
|
||||||
|
ranges.subsegment_ranges.emplace_back();
|
||||||
|
|
||||||
|
muxer_listener()->OnNewSegment(
|
||||||
|
options().output_file_name, 0,
|
||||||
|
duration_seconds * streams()[0]->time_scale(), size);
|
||||||
|
}
|
||||||
|
|
||||||
|
muxer_listener()->OnMediaEnd(ranges, duration_seconds);
|
||||||
|
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status WebVttMuxer::AddTextSample(size_t stream_id, const TextSample& sample) {
|
||||||
|
// Ignore sync samples.
|
||||||
|
if (sample.payload().empty()) {
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sample.id().find('\n') != std::string::npos ||
|
||||||
|
sample.settings().find('\n') != std::string::npos) {
|
||||||
|
return Status(error::MUXER_FAILURE,
|
||||||
|
"Text id/settings cannot contain newlines");
|
||||||
|
}
|
||||||
|
|
||||||
|
last_cue_ms_ = sample.EndTime();
|
||||||
|
buffer_->Append(sample);
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status WebVttMuxer::FinalizeSegment(size_t stream_id,
|
||||||
|
const SegmentInfo& segment_info) {
|
||||||
|
total_duration_ms_ += segment_info.duration;
|
||||||
|
|
||||||
|
const std::string& segment_template = options().segment_template;
|
||||||
|
DCHECK(!segment_template.empty());
|
||||||
|
const uint32_t index = segment_index_++;
|
||||||
|
const uint64_t start = segment_info.start_timestamp;
|
||||||
|
const uint64_t duration = segment_info.duration;
|
||||||
|
const uint32_t bandwidth = options().bandwidth;
|
||||||
|
|
||||||
|
const std::string filename =
|
||||||
|
GetSegmentName(segment_template, start, index, bandwidth);
|
||||||
|
uint64_t size;
|
||||||
|
RETURN_IF_ERROR(WriteToFile(filename, &size));
|
||||||
|
|
||||||
|
muxer_listener()->OnNewSegment(filename, start, duration, size);
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status WebVttMuxer::WriteToFile(const std::string& filename, uint64_t* size) {
|
||||||
|
// Write everything to the file before telling the manifest so that the
|
||||||
|
// file will exist on disk.
|
||||||
|
std::unique_ptr<File, FileCloser> file(File::Open(filename.c_str(), "w"));
|
||||||
|
if (!file) {
|
||||||
|
return Status(error::FILE_FAILURE, "Failed to open " + filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer_->WriteTo(file.get());
|
||||||
|
buffer_->Reset();
|
||||||
|
if (!file.release()->Close()) {
|
||||||
|
return Status(error::FILE_FAILURE, "Failed to close " + filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size) {
|
||||||
|
*size = File::GetFileSize(filename.c_str());
|
||||||
|
}
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace webvtt
|
||||||
|
} // namespace media
|
||||||
|
} // namespace shaka
|
|
@ -0,0 +1,47 @@
|
||||||
|
// Copyright 2020 Google LLC. All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file or at
|
||||||
|
// https://developers.google.com/open-source/licenses/bsd
|
||||||
|
|
||||||
|
#ifndef PACKAGER_MEDIA_FORMATS_WEBVTT_WEBVTT_MUXER_H_
|
||||||
|
#define PACKAGER_MEDIA_FORMATS_WEBVTT_WEBVTT_MUXER_H_
|
||||||
|
|
||||||
|
#include "packager/media/base/buffer_writer.h"
|
||||||
|
#include "packager/media/base/muxer.h"
|
||||||
|
#include "packager/media/formats/webvtt/webvtt_file_buffer.h"
|
||||||
|
|
||||||
|
namespace shaka {
|
||||||
|
namespace media {
|
||||||
|
namespace webvtt {
|
||||||
|
|
||||||
|
/// Implements WebVtt Muxer.
|
||||||
|
class WebVttMuxer : public Muxer {
|
||||||
|
public:
|
||||||
|
/// Create a WebMMuxer object from MuxerOptions.
|
||||||
|
explicit WebVttMuxer(const MuxerOptions& options);
|
||||||
|
~WebVttMuxer() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Muxer implementation overrides.
|
||||||
|
Status InitializeMuxer() override;
|
||||||
|
Status Finalize() override;
|
||||||
|
Status AddTextSample(size_t stream_id, const TextSample& sample) override;
|
||||||
|
Status FinalizeSegment(size_t stream_id,
|
||||||
|
const SegmentInfo& segment_info) override;
|
||||||
|
|
||||||
|
Status WriteToFile(const std::string& filename, uint64_t* size);
|
||||||
|
|
||||||
|
DISALLOW_COPY_AND_ASSIGN(WebVttMuxer);
|
||||||
|
|
||||||
|
std::unique_ptr<WebVttFileBuffer> buffer_;
|
||||||
|
uint64_t total_duration_ms_ = 0;
|
||||||
|
uint64_t last_cue_ms_ = 0;
|
||||||
|
uint32_t segment_index_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace webvtt
|
||||||
|
} // namespace media
|
||||||
|
} // namespace shaka
|
||||||
|
|
||||||
|
#endif // PACKAGER_MEDIA_FORMATS_WEBVTT_WEBVTT_MUXER_H_
|
|
@ -12,12 +12,15 @@
|
||||||
#include "packager/media/base/text_stream_info.h"
|
#include "packager/media/base/text_stream_info.h"
|
||||||
#include "packager/media/event/combined_muxer_listener.h"
|
#include "packager/media/event/combined_muxer_listener.h"
|
||||||
#include "packager/media/event/mock_muxer_listener.h"
|
#include "packager/media/event/mock_muxer_listener.h"
|
||||||
#include "packager/media/formats/webvtt/webvtt_text_output_handler.h"
|
#include "packager/media/formats/webvtt/webvtt_muxer.h"
|
||||||
#include "packager/status_test_util.h"
|
#include "packager/status_test_util.h"
|
||||||
|
|
||||||
namespace shaka {
|
namespace shaka {
|
||||||
namespace media {
|
namespace media {
|
||||||
|
namespace webvtt {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
using testing::_;
|
using testing::_;
|
||||||
|
|
||||||
const size_t kInputCount = 1;
|
const size_t kInputCount = 1;
|
||||||
|
@ -38,7 +41,7 @@ const uint64_t kSegmentDuration = 10000;
|
||||||
const float kMillisecondsPerSecond = 1000.0f;
|
const float kMillisecondsPerSecond = 1000.0f;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
class WebVttSegmentedOutputTest : public MediaHandlerTestBase {
|
class WebVttMuxerTest : public MediaHandlerTestBase {
|
||||||
protected:
|
protected:
|
||||||
void SetUp() {
|
void SetUp() {
|
||||||
MuxerOptions muxer_options;
|
MuxerOptions muxer_options;
|
||||||
|
@ -49,17 +52,17 @@ class WebVttSegmentedOutputTest : public MediaHandlerTestBase {
|
||||||
std::unique_ptr<MockMuxerListener> muxer_listener(new MockMuxerListener);
|
std::unique_ptr<MockMuxerListener> muxer_listener(new MockMuxerListener);
|
||||||
muxer_listener_ = muxer_listener.get();
|
muxer_listener_ = muxer_listener.get();
|
||||||
|
|
||||||
out_ = std::make_shared<WebVttTextOutputHandler>(muxer_options,
|
out_ = std::make_shared<WebVttMuxer>(muxer_options);
|
||||||
std::move(muxer_listener));
|
out_->SetMuxerListener(std::move(muxer_listener));
|
||||||
|
|
||||||
ASSERT_OK(SetUpAndInitializeGraph(out_, kInputCount, kOutputCount));
|
ASSERT_OK(SetUpAndInitializeGraph(out_, kInputCount, kOutputCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
MockMuxerListener* muxer_listener_ = nullptr;
|
MockMuxerListener* muxer_listener_ = nullptr;
|
||||||
std::shared_ptr<WebVttTextOutputHandler> out_;
|
std::shared_ptr<WebVttMuxer> out_;
|
||||||
};
|
};
|
||||||
|
|
||||||
TEST_F(WebVttSegmentedOutputTest, WithNoSegmentAndWithNoSamples) {
|
TEST_F(WebVttMuxerTest, WithNoSegmentAndWithNoSamples) {
|
||||||
EXPECT_CALL(*muxer_listener_, OnNewSegment(_, _, _, _)).Times(0);
|
EXPECT_CALL(*muxer_listener_, OnNewSegment(_, _, _, _)).Times(0);
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -67,10 +70,7 @@ TEST_F(WebVttSegmentedOutputTest, WithNoSegmentAndWithNoSamples) {
|
||||||
|
|
||||||
testing::InSequence s;
|
testing::InSequence s;
|
||||||
EXPECT_CALL(*muxer_listener_, OnMediaStart(_, _, _, _));
|
EXPECT_CALL(*muxer_listener_, OnMediaStart(_, _, _, _));
|
||||||
|
EXPECT_CALL(*muxer_listener_, OnMediaEndMock(_, _, _, _, _, _, _, _, _));
|
||||||
const float kMediaDuration = 0 * kSegmentDuration / kMillisecondsPerSecond;
|
|
||||||
EXPECT_CALL(*muxer_listener_,
|
|
||||||
OnMediaEndMock(_, _, _, _, _, _, _, _, kMediaDuration));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ASSERT_OK(Input(kInputIndex)
|
ASSERT_OK(Input(kInputIndex)
|
||||||
|
@ -79,7 +79,7 @@ TEST_F(WebVttSegmentedOutputTest, WithNoSegmentAndWithNoSamples) {
|
||||||
ASSERT_OK(Input(kInputIndex)->FlushAllDownstreams());
|
ASSERT_OK(Input(kInputIndex)->FlushAllDownstreams());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(WebVttSegmentedOutputTest, WithOneSegmentAndWithOneSample) {
|
TEST_F(WebVttMuxerTest, WithOneSegmentAndWithOneSample) {
|
||||||
const char* kExpectedOutput =
|
const char* kExpectedOutput =
|
||||||
"WEBVTT\n"
|
"WEBVTT\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
@ -118,7 +118,7 @@ TEST_F(WebVttSegmentedOutputTest, WithOneSegmentAndWithOneSample) {
|
||||||
ASSERT_FILE_STREQ(kSegmentedFileOutput1, kExpectedOutput);
|
ASSERT_FILE_STREQ(kSegmentedFileOutput1, kExpectedOutput);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(WebVttSegmentedOutputTest, WithTwoSegmentAndWithOneSample) {
|
TEST_F(WebVttMuxerTest, WithTwoSegmentAndWithOneSample) {
|
||||||
const char* kExpectedOutput1 =
|
const char* kExpectedOutput1 =
|
||||||
"WEBVTT\n"
|
"WEBVTT\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
@ -181,7 +181,7 @@ TEST_F(WebVttSegmentedOutputTest, WithTwoSegmentAndWithOneSample) {
|
||||||
ASSERT_FILE_STREQ(kSegmentedFileOutput2, kExpectedOutput2);
|
ASSERT_FILE_STREQ(kSegmentedFileOutput2, kExpectedOutput2);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(WebVttSegmentedOutputTest, WithAnEmptySegment) {
|
TEST_F(WebVttMuxerTest, WithAnEmptySegment) {
|
||||||
const char* kExpectedOutput1 =
|
const char* kExpectedOutput1 =
|
||||||
"WEBVTT\n"
|
"WEBVTT\n"
|
||||||
"\n";
|
"\n";
|
||||||
|
@ -235,5 +235,7 @@ TEST_F(WebVttSegmentedOutputTest, WithAnEmptySegment) {
|
||||||
ASSERT_FILE_STREQ(kSegmentedFileOutput1, kExpectedOutput1);
|
ASSERT_FILE_STREQ(kSegmentedFileOutput1, kExpectedOutput1);
|
||||||
ASSERT_FILE_STREQ(kSegmentedFileOutput2, kExpectedOutput2);
|
ASSERT_FILE_STREQ(kSegmentedFileOutput2, kExpectedOutput2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace webvtt
|
||||||
} // namespace media
|
} // namespace media
|
||||||
} // namespace shaka
|
} // namespace shaka
|
|
@ -1,134 +0,0 @@
|
||||||
// Copyright 2017 Google Inc. All rights reserved.
|
|
||||||
//
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file or at
|
|
||||||
// https://developers.google.com/open-source/licenses/bsd
|
|
||||||
|
|
||||||
#include "packager/media/formats/webvtt/webvtt_text_output_handler.h"
|
|
||||||
|
|
||||||
#include <algorithm> // needed for min and max
|
|
||||||
|
|
||||||
#include "packager/base/logging.h"
|
|
||||||
#include "packager/file/file.h"
|
|
||||||
#include "packager/file/file_closer.h"
|
|
||||||
#include "packager/media/base/muxer_util.h"
|
|
||||||
#include "packager/status_macros.h"
|
|
||||||
|
|
||||||
namespace shaka {
|
|
||||||
namespace media {
|
|
||||||
namespace {
|
|
||||||
double kMillisecondsToSeconds = 1000.0;
|
|
||||||
|
|
||||||
std::string ToString(const std::vector<uint8_t>& v) {
|
|
||||||
return std::string(v.begin(), v.end());
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
WebVttTextOutputHandler::WebVttTextOutputHandler(
|
|
||||||
const MuxerOptions& muxer_options,
|
|
||||||
std::unique_ptr<MuxerListener> muxer_listener)
|
|
||||||
: muxer_options_(muxer_options),
|
|
||||||
muxer_listener_(std::move(muxer_listener)) {}
|
|
||||||
|
|
||||||
Status WebVttTextOutputHandler::InitializeInternal() {
|
|
||||||
return Status::OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
Status WebVttTextOutputHandler::Process(
|
|
||||||
std::unique_ptr<StreamData> stream_data) {
|
|
||||||
switch (stream_data->stream_data_type) {
|
|
||||||
case StreamDataType::kStreamInfo:
|
|
||||||
return OnStreamInfo(*stream_data->stream_info);
|
|
||||||
case StreamDataType::kSegmentInfo:
|
|
||||||
return OnSegmentInfo(*stream_data->segment_info);
|
|
||||||
case StreamDataType::kCueEvent:
|
|
||||||
return OnCueEvent(*stream_data->cue_event);
|
|
||||||
case StreamDataType::kTextSample:
|
|
||||||
OnTextSample(*stream_data->text_sample);
|
|
||||||
return Status::OK;
|
|
||||||
default:
|
|
||||||
return Status(error::INTERNAL_ERROR,
|
|
||||||
"Invalid stream data type for this handler");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Status WebVttTextOutputHandler::OnFlushRequest(size_t input_stream_index) {
|
|
||||||
if (!buffer_) {
|
|
||||||
LOG(INFO) << "Skip stream '" << muxer_options_.segment_template
|
|
||||||
<< "' which does not contain any sample.";
|
|
||||||
return Status::OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
DCHECK_EQ(buffer_->sample_count(), 0u)
|
|
||||||
<< "There should have been a segment info before flushing that would "
|
|
||||||
"have cleared out all the samples.";
|
|
||||||
|
|
||||||
const float duration_ms = static_cast<float>(total_duration_ms_);
|
|
||||||
const float duration_seconds = duration_ms / 1000.0f;
|
|
||||||
|
|
||||||
MuxerListener::MediaRanges empty_ranges;
|
|
||||||
muxer_listener_->OnMediaEnd(empty_ranges, duration_seconds);
|
|
||||||
|
|
||||||
return Status::OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
Status WebVttTextOutputHandler::OnStreamInfo(const StreamInfo& info) {
|
|
||||||
buffer_.reset(
|
|
||||||
new WebVttFileBuffer(muxer_options_.transport_stream_timestamp_offset_ms,
|
|
||||||
ToString(info.codec_config())));
|
|
||||||
muxer_listener_->OnMediaStart(muxer_options_, info, info.time_scale(),
|
|
||||||
MuxerListener::kContainerText);
|
|
||||||
return Status::OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
Status WebVttTextOutputHandler::OnSegmentInfo(const SegmentInfo& info) {
|
|
||||||
total_duration_ms_ += info.duration;
|
|
||||||
|
|
||||||
const std::string& segment_template = muxer_options_.segment_template;
|
|
||||||
const uint32_t index = segment_index_++;
|
|
||||||
const uint64_t start = info.start_timestamp;
|
|
||||||
const uint64_t duration = info.duration;
|
|
||||||
const uint32_t bandwidth = muxer_options_.bandwidth;
|
|
||||||
|
|
||||||
const std::string filename =
|
|
||||||
GetSegmentName(segment_template, start, index, bandwidth);
|
|
||||||
|
|
||||||
// Write everything to the file before telling the manifest so that the
|
|
||||||
// file will exist on disk.
|
|
||||||
std::unique_ptr<File, FileCloser> file(File::Open(filename.c_str(), "w"));
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
return Status(error::FILE_FAILURE, "Failed to open " + filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer_->WriteTo(file.get());
|
|
||||||
buffer_->Reset();
|
|
||||||
|
|
||||||
if (!file.release()->Close()) {
|
|
||||||
return Status(error::FILE_FAILURE, "Failed to close " + filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the manifest with our new file.
|
|
||||||
const uint64_t size = File::GetFileSize(filename.c_str());
|
|
||||||
muxer_listener_->OnNewSegment(filename, start, duration, size);
|
|
||||||
|
|
||||||
return Status::OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
Status WebVttTextOutputHandler::OnCueEvent(const CueEvent& event) {
|
|
||||||
double timestamp_seconds = event.time_in_seconds;
|
|
||||||
double timestamp_ms = timestamp_seconds * kMillisecondsToSeconds;
|
|
||||||
uint64_t timestamp = static_cast<uint64_t>(timestamp_ms);
|
|
||||||
muxer_listener_->OnCueEvent(timestamp, event.cue_data);
|
|
||||||
return Status::OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
void WebVttTextOutputHandler::OnTextSample(const TextSample& sample) {
|
|
||||||
// Skip empty samples. It is normal to see empty samples as earlier in the
|
|
||||||
// pipeline we pad the stream to remove gaps.
|
|
||||||
if (sample.payload().size()) {
|
|
||||||
buffer_->Append(sample);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace media
|
|
||||||
} // namespace shaka
|
|
|
@ -1,58 +0,0 @@
|
||||||
// Copyright 2017 Google Inc. All rights reserved.
|
|
||||||
//
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file or at
|
|
||||||
// https://developers.google.com/open-source/licenses/bsd
|
|
||||||
|
|
||||||
#ifndef PACKAGER_MEDIA_FORMATS_WEBVTT_WEBVTT_TEXT_HANDLER_H_
|
|
||||||
#define PACKAGER_MEDIA_FORMATS_WEBVTT_WEBVTT_TEXT_HANDLER_H_
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "packager/media/base/media_handler.h"
|
|
||||||
#include "packager/media/base/muxer_options.h"
|
|
||||||
#include "packager/media/event/muxer_listener.h"
|
|
||||||
#include "packager/media/formats/webvtt/webvtt_file_buffer.h"
|
|
||||||
|
|
||||||
namespace shaka {
|
|
||||||
namespace media {
|
|
||||||
|
|
||||||
class WebVttTextOutputHandler : public MediaHandler {
|
|
||||||
public:
|
|
||||||
WebVttTextOutputHandler(const MuxerOptions& muxer_options,
|
|
||||||
std::unique_ptr<MuxerListener> muxer_listener);
|
|
||||||
virtual ~WebVttTextOutputHandler() = default;
|
|
||||||
|
|
||||||
private:
|
|
||||||
WebVttTextOutputHandler(const WebVttTextOutputHandler&) = delete;
|
|
||||||
WebVttTextOutputHandler& operator=(const WebVttTextOutputHandler&) = delete;
|
|
||||||
|
|
||||||
Status InitializeInternal() override;
|
|
||||||
Status Process(std::unique_ptr<StreamData> stream_data) override;
|
|
||||||
Status OnFlushRequest(size_t input_stream_index) override;
|
|
||||||
|
|
||||||
Status OnStreamInfo(const StreamInfo& info);
|
|
||||||
Status OnSegmentInfo(const SegmentInfo& info);
|
|
||||||
Status OnCueEvent(const CueEvent& event);
|
|
||||||
void OnTextSample(const TextSample& sample);
|
|
||||||
|
|
||||||
Status OnSegmentEnded();
|
|
||||||
|
|
||||||
void GoToNextSegment(uint64_t start_time_ms);
|
|
||||||
|
|
||||||
const MuxerOptions muxer_options_;
|
|
||||||
std::unique_ptr<MuxerListener> muxer_listener_;
|
|
||||||
|
|
||||||
// Sum together all segment durations so we know how long the stream is.
|
|
||||||
uint64_t total_duration_ms_ = 0;
|
|
||||||
uint32_t segment_index_ = 0;
|
|
||||||
|
|
||||||
std::unique_ptr<WebVttFileBuffer> buffer_;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace media
|
|
||||||
} // namespace shaka
|
|
||||||
|
|
||||||
#endif // PACKAGER_MEDIA_FORMATS_WEBVTT_WEBVTT_TEXT_HANDLER_H_
|
|
|
@ -367,9 +367,9 @@ bool RepresentationXmlNode::AddVODOnlyInfo(const MediaInfo& media_info) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool need_segment_base = media_info.has_index_range() ||
|
const bool need_segment_base =
|
||||||
media_info.has_init_range() ||
|
media_info.has_index_range() || media_info.has_init_range() ||
|
||||||
media_info.has_reference_time_scale();
|
(media_info.has_reference_time_scale() && !media_info.has_text_info());
|
||||||
|
|
||||||
if (need_segment_base) {
|
if (need_segment_base) {
|
||||||
XmlNode segment_base("SegmentBase");
|
XmlNode segment_base("SegmentBase");
|
||||||
|
@ -438,12 +438,12 @@ bool RepresentationXmlNode::AddLiveOnlyInfo(
|
||||||
segment_infos.front().duration);
|
segment_infos.front().duration);
|
||||||
if (FLAGS_dash_add_last_segment_number_when_needed) {
|
if (FLAGS_dash_add_last_segment_number_when_needed) {
|
||||||
uint32_t last_segment_number = start_number - 1;
|
uint32_t last_segment_number = start_number - 1;
|
||||||
for (const auto& segment_info_element : segment_infos)
|
for (const auto& segment_info_element : segment_infos)
|
||||||
last_segment_number += segment_info_element.repeat + 1;
|
last_segment_number += segment_info_element.repeat + 1;
|
||||||
|
|
||||||
AddSupplementalProperty(
|
AddSupplementalProperty(
|
||||||
"http://dashif.org/guidelines/last-segment-number",
|
"http://dashif.org/guidelines/last-segment-number",
|
||||||
std::to_string(last_segment_number));
|
std::to_string(last_segment_number));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
XmlNode segment_timeline("SegmentTimeline");
|
XmlNode segment_timeline("SegmentTimeline");
|
||||||
|
|
|
@ -40,7 +40,6 @@
|
||||||
#include "packager/media/event/muxer_listener_factory.h"
|
#include "packager/media/event/muxer_listener_factory.h"
|
||||||
#include "packager/media/event/vod_media_info_dump_muxer_listener.h"
|
#include "packager/media/event/vod_media_info_dump_muxer_listener.h"
|
||||||
#include "packager/media/formats/webvtt/text_padder.h"
|
#include "packager/media/formats/webvtt/text_padder.h"
|
||||||
#include "packager/media/formats/webvtt/webvtt_text_output_handler.h"
|
|
||||||
#include "packager/media/formats/webvtt/webvtt_to_mp4_handler.h"
|
#include "packager/media/formats/webvtt/webvtt_to_mp4_handler.h"
|
||||||
#include "packager/media/replicator/replicator.h"
|
#include "packager/media/replicator/replicator.h"
|
||||||
#include "packager/media/trick_play/trick_play_handler.h"
|
#include "packager/media/trick_play/trick_play_handler.h"
|
||||||
|
@ -66,21 +65,6 @@ const char kMediaInfoSuffix[] = ".media_info";
|
||||||
|
|
||||||
const int64_t kDefaultTextZeroBiasMs = 10 * 60 * 1000; // 10 minutes
|
const int64_t kDefaultTextZeroBiasMs = 10 * 60 * 1000; // 10 minutes
|
||||||
|
|
||||||
MuxerOptions CreateMuxerOptions(const StreamDescriptor& stream,
|
|
||||||
const PackagingParams& params) {
|
|
||||||
MuxerOptions options;
|
|
||||||
|
|
||||||
options.mp4_params = params.mp4_output_params;
|
|
||||||
options.transport_stream_timestamp_offset_ms =
|
|
||||||
params.transport_stream_timestamp_offset_ms;
|
|
||||||
options.temp_dir = params.temp_dir;
|
|
||||||
options.bandwidth = stream.bandwidth;
|
|
||||||
options.output_file_name = stream.output;
|
|
||||||
options.segment_template = stream.segment_template;
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
MuxerListenerFactory::StreamData ToMuxerListenerData(
|
MuxerListenerFactory::StreamData ToMuxerListenerData(
|
||||||
const StreamDescriptor& stream) {
|
const StreamDescriptor& stream) {
|
||||||
MuxerListenerFactory::StreamData data;
|
MuxerListenerFactory::StreamData data;
|
||||||
|
@ -486,99 +470,30 @@ std::unique_ptr<MediaHandler> CreateTextChunker(
|
||||||
new TextChunker(segment_length_in_seconds));
|
new TextChunker(segment_length_in_seconds));
|
||||||
}
|
}
|
||||||
|
|
||||||
Status CreateHlsTextJob(const StreamDescriptor& stream,
|
Status CreateTtmlJobs(
|
||||||
const PackagingParams& packaging_params,
|
|
||||||
std::unique_ptr<MuxerListener> muxer_listener,
|
|
||||||
SyncPointQueue* sync_points,
|
|
||||||
JobManager* job_manager) {
|
|
||||||
DCHECK(muxer_listener);
|
|
||||||
DCHECK(job_manager);
|
|
||||||
|
|
||||||
if (stream.segment_template.empty()) {
|
|
||||||
return Status(error::INVALID_ARGUMENT,
|
|
||||||
"Cannot output text (" + stream.input +
|
|
||||||
") to HLS with no segment template");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Text files are usually small and since the input is one file;
|
|
||||||
// there's no way for the player to do ranged requests. So set this
|
|
||||||
// value to something reasonable if it is missing.
|
|
||||||
MuxerOptions muxer_options = CreateMuxerOptions(stream, packaging_params);
|
|
||||||
muxer_options.bandwidth = stream.bandwidth ? stream.bandwidth : 256;
|
|
||||||
|
|
||||||
auto output = std::make_shared<WebVttTextOutputHandler>(
|
|
||||||
muxer_options, std::move(muxer_listener));
|
|
||||||
|
|
||||||
std::shared_ptr<Demuxer> demuxer;
|
|
||||||
RETURN_IF_ERROR(CreateDemuxer(stream, packaging_params, &demuxer));
|
|
||||||
if (!stream.language.empty())
|
|
||||||
demuxer->SetLanguageOverride(stream.stream_selector, stream.language);
|
|
||||||
auto padder = std::make_shared<TextPadder>(kDefaultTextZeroBiasMs);
|
|
||||||
RETURN_IF_ERROR(demuxer->SetHandler(stream.stream_selector, padder));
|
|
||||||
|
|
||||||
auto cue_aligner = sync_points
|
|
||||||
? std::make_shared<CueAlignmentHandler>(sync_points)
|
|
||||||
: nullptr;
|
|
||||||
auto chunker = CreateTextChunker(packaging_params.chunking_params);
|
|
||||||
|
|
||||||
job_manager->Add("Segmented Text Job", demuxer);
|
|
||||||
|
|
||||||
return MediaHandler::Chain({std::move(padder), std::move(cue_aligner),
|
|
||||||
std::move(chunker), std::move(output)});
|
|
||||||
}
|
|
||||||
|
|
||||||
Status CreateTextJobs(
|
|
||||||
const std::vector<std::reference_wrapper<const StreamDescriptor>>& streams,
|
const std::vector<std::reference_wrapper<const StreamDescriptor>>& streams,
|
||||||
const PackagingParams& packaging_params,
|
const PackagingParams& packaging_params,
|
||||||
SyncPointQueue* sync_points,
|
SyncPointQueue* sync_points,
|
||||||
MuxerListenerFactory* muxer_listener_factory,
|
|
||||||
MuxerFactory* muxer_factory,
|
MuxerFactory* muxer_factory,
|
||||||
MpdNotifier* mpd_notifier,
|
MpdNotifier* mpd_notifier,
|
||||||
JobManager* job_manager) {
|
JobManager* job_manager) {
|
||||||
DCHECK(muxer_listener_factory);
|
|
||||||
DCHECK(job_manager);
|
DCHECK(job_manager);
|
||||||
for (const StreamDescriptor& stream : streams) {
|
for (const StreamDescriptor& stream : streams) {
|
||||||
// There are currently options:
|
|
||||||
// TEXT TTML --> TEXT TTML [ supported ], for DASH only.
|
|
||||||
// TEXT WEBVTT --> TEXT WEBVTT [ supported ]
|
|
||||||
// TEXT WEBVTT --> MP4 WEBVTT [ supported ]
|
|
||||||
// MP4 WEBVTT --> MP4 WEBVTT [ unsupported ]
|
|
||||||
// MP4 WEBVTT --> TEXT WEBVTT [ unsupported ]
|
|
||||||
const auto input_container = DetermineContainerFromFileName(stream.input);
|
|
||||||
|
|
||||||
if (input_container != CONTAINER_WEBVTT &&
|
|
||||||
input_container != CONTAINER_TTML) {
|
|
||||||
return Status(error::INVALID_ARGUMENT,
|
|
||||||
"Text output format is not support for " + stream.input);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<MuxerListener> hls_listener =
|
|
||||||
muxer_listener_factory->CreateHlsListener(ToMuxerListenerData(stream));
|
|
||||||
|
|
||||||
// Check input to ensure that output is possible.
|
// Check input to ensure that output is possible.
|
||||||
if (hls_listener && !stream.dash_only) {
|
if (!packaging_params.hls_params.master_playlist_output.empty() &&
|
||||||
if (input_container == CONTAINER_TTML) {
|
!stream.dash_only) {
|
||||||
return Status(error::INVALID_ARGUMENT,
|
|
||||||
"HLS does not support TTML in xml format.");
|
|
||||||
}
|
|
||||||
if (stream.segment_template.empty() || !stream.output.empty()) {
|
|
||||||
return Status(error::INVALID_ARGUMENT,
|
|
||||||
"segment_template needs to be specified for HLS text "
|
|
||||||
"output. Single file output is not supported yet.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mpd_notifier && !stream.segment_template.empty() && !stream.hls_only) {
|
|
||||||
return Status(error::INVALID_ARGUMENT,
|
return Status(error::INVALID_ARGUMENT,
|
||||||
"Cannot create text output for MPD with segment output.");
|
"HLS does not support TTML in xml format.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we are outputting to HLS, then create the HLS test pipeline that
|
if (!stream.segment_template.empty()) {
|
||||||
// will create segmented text output.
|
return Status(error::INVALID_ARGUMENT,
|
||||||
if (hls_listener && !stream.dash_only) {
|
"Segmented TTML is not supported.");
|
||||||
RETURN_IF_ERROR(CreateHlsTextJob(stream, packaging_params,
|
}
|
||||||
std::move(hls_listener), sync_points,
|
|
||||||
job_manager));
|
if (GetOutputFormat(stream) != CONTAINER_TTML) {
|
||||||
|
return Status(error::INVALID_ARGUMENT,
|
||||||
|
"Converting TTML to other formats is not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stream.output.empty()) {
|
if (!stream.output.empty()) {
|
||||||
|
@ -694,10 +609,7 @@ Status CreateAudioVideoJobs(
|
||||||
if (sync_points) {
|
if (sync_points) {
|
||||||
handlers.emplace_back(cue_aligner);
|
handlers.emplace_back(cue_aligner);
|
||||||
}
|
}
|
||||||
if (is_text) {
|
if (!is_text) {
|
||||||
handlers.emplace_back(
|
|
||||||
CreateTextChunker(packaging_params.chunking_params));
|
|
||||||
} else {
|
|
||||||
handlers.emplace_back(std::make_shared<ChunkingHandler>(
|
handlers.emplace_back(std::make_shared<ChunkingHandler>(
|
||||||
packaging_params.chunking_params));
|
packaging_params.chunking_params));
|
||||||
}
|
}
|
||||||
|
@ -731,6 +643,12 @@ Status CreateAudioVideoJobs(
|
||||||
? std::make_shared<TrickPlayHandler>(stream.trick_play_factor)
|
? std::make_shared<TrickPlayHandler>(stream.trick_play_factor)
|
||||||
: nullptr;
|
: nullptr;
|
||||||
|
|
||||||
|
std::shared_ptr<MediaHandler> chunker =
|
||||||
|
is_text && (!stream.segment_template.empty() ||
|
||||||
|
output_format == CONTAINER_MOV)
|
||||||
|
? CreateTextChunker(packaging_params.chunking_params)
|
||||||
|
: nullptr;
|
||||||
|
|
||||||
// TODO(modmaker): Move to MOV muxer?
|
// TODO(modmaker): Move to MOV muxer?
|
||||||
const auto input_container = DetermineContainerFromFileName(stream.input);
|
const auto input_container = DetermineContainerFromFileName(stream.input);
|
||||||
auto text_to_mp4 =
|
auto text_to_mp4 =
|
||||||
|
@ -738,8 +656,8 @@ Status CreateAudioVideoJobs(
|
||||||
? std::make_shared<WebVttToMp4Handler>()
|
? std::make_shared<WebVttToMp4Handler>()
|
||||||
: nullptr;
|
: nullptr;
|
||||||
|
|
||||||
RETURN_IF_ERROR(
|
RETURN_IF_ERROR(MediaHandler::Chain(
|
||||||
MediaHandler::Chain({replicator, trick_play, text_to_mp4, muxer}));
|
{replicator, trick_play, chunker, text_to_mp4, muxer}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Status::OK;
|
return Status::OK;
|
||||||
|
@ -758,7 +676,7 @@ Status CreateAllJobs(const std::vector<StreamDescriptor>& stream_descriptors,
|
||||||
DCHECK(job_manager);
|
DCHECK(job_manager);
|
||||||
|
|
||||||
// Group all streams based on which pipeline they will use.
|
// Group all streams based on which pipeline they will use.
|
||||||
std::vector<std::reference_wrapper<const StreamDescriptor>> text_streams;
|
std::vector<std::reference_wrapper<const StreamDescriptor>> ttml_streams;
|
||||||
std::vector<std::reference_wrapper<const StreamDescriptor>>
|
std::vector<std::reference_wrapper<const StreamDescriptor>>
|
||||||
audio_video_streams;
|
audio_video_streams;
|
||||||
|
|
||||||
|
@ -766,15 +684,12 @@ Status CreateAllJobs(const std::vector<StreamDescriptor>& stream_descriptors,
|
||||||
bool has_non_transport_audio_video_streams = false;
|
bool has_non_transport_audio_video_streams = false;
|
||||||
|
|
||||||
for (const StreamDescriptor& stream : stream_descriptors) {
|
for (const StreamDescriptor& stream : stream_descriptors) {
|
||||||
// TODO: Find a better way to determine what stream type a stream
|
const auto input_container = DetermineContainerFromFileName(stream.input);
|
||||||
// descriptor is as |stream_selector| may use an index. This would
|
|
||||||
// also allow us to use a simpler audio pipeline.
|
|
||||||
const auto output_format = GetOutputFormat(stream);
|
const auto output_format = GetOutputFormat(stream);
|
||||||
if (stream.stream_selector == "text" && output_format != CONTAINER_MOV) {
|
if (input_container == CONTAINER_TTML) {
|
||||||
text_streams.push_back(stream);
|
ttml_streams.push_back(stream);
|
||||||
} else {
|
} else {
|
||||||
audio_video_streams.push_back(stream);
|
audio_video_streams.push_back(stream);
|
||||||
|
|
||||||
switch (output_format) {
|
switch (output_format) {
|
||||||
case CONTAINER_MPEG2TS:
|
case CONTAINER_MPEG2TS:
|
||||||
case CONTAINER_AAC:
|
case CONTAINER_AAC:
|
||||||
|
@ -783,6 +698,9 @@ Status CreateAllJobs(const std::vector<StreamDescriptor>& stream_descriptors,
|
||||||
case CONTAINER_EAC3:
|
case CONTAINER_EAC3:
|
||||||
has_transport_audio_video_streams = true;
|
has_transport_audio_video_streams = true;
|
||||||
break;
|
break;
|
||||||
|
case CONTAINER_TTML:
|
||||||
|
case CONTAINER_WEBVTT:
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
has_non_transport_audio_video_streams = true;
|
has_non_transport_audio_video_streams = true;
|
||||||
break;
|
break;
|
||||||
|
@ -795,26 +713,21 @@ Status CreateAllJobs(const std::vector<StreamDescriptor>& stream_descriptors,
|
||||||
std::sort(audio_video_streams.begin(), audio_video_streams.end(),
|
std::sort(audio_video_streams.begin(), audio_video_streams.end(),
|
||||||
media::StreamDescriptorCompareFn);
|
media::StreamDescriptorCompareFn);
|
||||||
|
|
||||||
if (!text_streams.empty()) {
|
if (packaging_params.transport_stream_timestamp_offset_ms > 0) {
|
||||||
PackagingParams text_packaging_params = packaging_params;
|
if (has_transport_audio_video_streams &&
|
||||||
if (text_packaging_params.transport_stream_timestamp_offset_ms > 0) {
|
has_non_transport_audio_video_streams) {
|
||||||
if (has_transport_audio_video_streams &&
|
LOG(WARNING) << "There may be problems mixing transport streams and "
|
||||||
has_non_transport_audio_video_streams) {
|
"non-transport streams. For example, the subtitles may "
|
||||||
LOG(WARNING) << "There may be problems mixing transport streams and "
|
"be out of sync with non-transport streams.";
|
||||||
"non-transport streams. For example, the subtitles may "
|
} else if (has_non_transport_audio_video_streams) {
|
||||||
"be out of sync with non-transport streams.";
|
// Don't insert the X-TIMESTAMP-MAP in WebVTT if there is no transport
|
||||||
} else if (has_non_transport_audio_video_streams) {
|
// stream.
|
||||||
// Don't insert the X-TIMESTAMP-MAP in WebVTT if there is no transport
|
muxer_factory->SetTsStreamOffset(0);
|
||||||
// stream.
|
|
||||||
text_packaging_params.transport_stream_timestamp_offset_ms = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_IF_ERROR(CreateTextJobs(text_streams, text_packaging_params,
|
|
||||||
sync_points, muxer_listener_factory,
|
|
||||||
muxer_factory, mpd_notifier, job_manager));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RETURN_IF_ERROR(CreateTtmlJobs(ttml_streams, packaging_params, sync_points,
|
||||||
|
muxer_factory, mpd_notifier, job_manager));
|
||||||
RETURN_IF_ERROR(CreateAudioVideoJobs(
|
RETURN_IF_ERROR(CreateAudioVideoJobs(
|
||||||
audio_video_streams, packaging_params, encryption_key_source, sync_points,
|
audio_video_streams, packaging_params, encryption_key_source, sync_points,
|
||||||
muxer_listener_factory, muxer_factory, job_manager));
|
muxer_listener_factory, muxer_factory, job_manager));
|
||||||
|
|
Loading…
Reference in New Issue