VOD text support
Change-Id: Ic69169c31d0a54de4895d49ff8b4a758039733a7
This commit is contained in:
parent
4c10755d40
commit
29e14a3d6b
|
@ -22,14 +22,17 @@
|
||||||
#include "packager/base/strings/stringprintf.h"
|
#include "packager/base/strings/stringprintf.h"
|
||||||
#include "packager/base/threading/simple_thread.h"
|
#include "packager/base/threading/simple_thread.h"
|
||||||
#include "packager/base/time/clock.h"
|
#include "packager/base/time/clock.h"
|
||||||
|
#include "packager/media/base/container_names.h"
|
||||||
#include "packager/media/base/demuxer.h"
|
#include "packager/media/base/demuxer.h"
|
||||||
#include "packager/media/base/key_source.h"
|
#include "packager/media/base/key_source.h"
|
||||||
#include "packager/media/base/muxer_options.h"
|
#include "packager/media/base/muxer_options.h"
|
||||||
#include "packager/media/base/muxer_util.h"
|
#include "packager/media/base/muxer_util.h"
|
||||||
#include "packager/media/event/mpd_notify_muxer_listener.h"
|
#include "packager/media/event/mpd_notify_muxer_listener.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/file/file.h"
|
||||||
#include "packager/media/formats/mp4/mp4_muxer.h"
|
#include "packager/media/formats/mp4/mp4_muxer.h"
|
||||||
#include "packager/mpd/base/dash_iop_mpd_notifier.h"
|
#include "packager/mpd/base/dash_iop_mpd_notifier.h"
|
||||||
|
#include "packager/mpd/base/media_info.pb.h"
|
||||||
#include "packager/mpd/base/mpd_builder.h"
|
#include "packager/mpd/base/mpd_builder.h"
|
||||||
#include "packager/mpd/base/simple_mpd_notifier.h"
|
#include "packager/mpd/base/simple_mpd_notifier.h"
|
||||||
|
|
||||||
|
@ -64,6 +67,8 @@ const char kUsage[] =
|
||||||
"language tag. If specified, this value overrides any language metadata "
|
"language tag. If specified, this value overrides any language metadata "
|
||||||
"in the input track.\n";
|
"in the input track.\n";
|
||||||
|
|
||||||
|
const char kMediaInfoSuffix[] = ".media_info";
|
||||||
|
|
||||||
enum ExitStatus {
|
enum ExitStatus {
|
||||||
kSuccess = 0,
|
kSuccess = 0,
|
||||||
kNoArgument,
|
kNoArgument,
|
||||||
|
@ -71,6 +76,29 @@ enum ExitStatus {
|
||||||
kPackagingFailed,
|
kPackagingFailed,
|
||||||
kInternalError,
|
kInternalError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO(rkuroiwa): Write TTML and WebVTT parser (demuxing) for a better check
|
||||||
|
// and for supporting live/segmenting (muxing). With a demuxer and a muxer,
|
||||||
|
// CreateRemuxJobs() shouldn't treat text as a special case.
|
||||||
|
std::string DetermineTextFileFormat(const std::string& file) {
|
||||||
|
std::string content;
|
||||||
|
if (!edash_packager::media::File::ReadFileToString(file.c_str(), &content)) {
|
||||||
|
LOG(ERROR) << "Failed to open file " << file
|
||||||
|
<< " to determine file format.";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
edash_packager::media::MediaContainerName container_name =
|
||||||
|
edash_packager::media::DetermineContainer(
|
||||||
|
reinterpret_cast<const uint8_t*>(content.data()), content.size());
|
||||||
|
if (container_name == edash_packager::media::CONTAINER_WEBVTT) {
|
||||||
|
return "vtt";
|
||||||
|
} else if (container_name == edash_packager::media::CONTAINER_TTML) {
|
||||||
|
return "ttml";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace edash_packager {
|
namespace edash_packager {
|
||||||
|
@ -114,6 +142,45 @@ class RemuxJob : public base::SimpleThread {
|
||||||
DISALLOW_COPY_AND_ASSIGN(RemuxJob);
|
DISALLOW_COPY_AND_ASSIGN(RemuxJob);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
bool StreamInfoToTextMediaInfo(const StreamDescriptor& stream_descriptor,
|
||||||
|
const MuxerOptions& stream_muxer_options,
|
||||||
|
MediaInfo* text_media_info) {
|
||||||
|
const std::string& language = stream_descriptor.language;
|
||||||
|
std::string format = DetermineTextFileFormat(stream_descriptor.input);
|
||||||
|
if (format.empty()) {
|
||||||
|
LOG(ERROR) << "Failed to determine the text file format for "
|
||||||
|
<< stream_descriptor.input;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File::Copy(stream_descriptor.input.c_str(),
|
||||||
|
stream_muxer_options.output_file_name.c_str())) {
|
||||||
|
LOG(ERROR) << "Failed to copy the input file (" << stream_descriptor.input
|
||||||
|
<< ") to output file (" << stream_muxer_options.output_file_name
|
||||||
|
<< ").";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
text_media_info->set_media_file_name(stream_muxer_options.output_file_name);
|
||||||
|
text_media_info->set_container_type(MediaInfo::CONTAINER_TEXT);
|
||||||
|
|
||||||
|
if (stream_muxer_options.bandwidth != 0) {
|
||||||
|
text_media_info->set_bandwidth(stream_muxer_options.bandwidth);
|
||||||
|
} else {
|
||||||
|
// 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.
|
||||||
|
text_media_info->set_bandwidth(256);
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaInfo::TextInfo* text_info = text_media_info->mutable_text_info();
|
||||||
|
text_info->set_format(format);
|
||||||
|
if (!language.empty())
|
||||||
|
text_info->set_language(language);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors,
|
bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors,
|
||||||
const MuxerOptions& muxer_options,
|
const MuxerOptions& muxer_options,
|
||||||
FakeClock* fake_clock,
|
FakeClock* fake_clock,
|
||||||
|
@ -140,6 +207,34 @@ bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors,
|
||||||
}
|
}
|
||||||
stream_muxer_options.bandwidth = stream_iter->bandwidth;
|
stream_muxer_options.bandwidth = stream_iter->bandwidth;
|
||||||
|
|
||||||
|
// Handle text input.
|
||||||
|
if (stream_iter->stream_selector == "text") {
|
||||||
|
MediaInfo text_media_info;
|
||||||
|
if (!StreamInfoToTextMediaInfo(*stream_iter, stream_muxer_options,
|
||||||
|
&text_media_info)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mpd_notifier) {
|
||||||
|
uint32 unused;
|
||||||
|
if (!mpd_notifier->NotifyNewContainer(text_media_info, &unused)) {
|
||||||
|
LOG(ERROR) << "Failed to process text file " << stream_iter->input;
|
||||||
|
} else {
|
||||||
|
mpd_notifier->Flush();
|
||||||
|
}
|
||||||
|
} else if (FLAGS_output_media_info) {
|
||||||
|
VodMediaInfoDumpMuxerListener::WriteMediaInfoToFile(
|
||||||
|
text_media_info,
|
||||||
|
stream_muxer_options.output_file_name + kMediaInfoSuffix);
|
||||||
|
} else {
|
||||||
|
NOTIMPLEMENTED()
|
||||||
|
<< "--mpd_output or --output_media_info flags are "
|
||||||
|
"required for text output. Skipping manifest related output for "
|
||||||
|
<< stream_iter->input;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (stream_iter->input != previous_input) {
|
if (stream_iter->input != previous_input) {
|
||||||
// New remux job needed. Create demux and job thread.
|
// New remux job needed. Create demux and job thread.
|
||||||
scoped_ptr<Demuxer> demuxer(new Demuxer(stream_iter->input));
|
scoped_ptr<Demuxer> demuxer(new Demuxer(stream_iter->input));
|
||||||
|
@ -180,7 +275,7 @@ bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors,
|
||||||
DCHECK(!(FLAGS_output_media_info && mpd_notifier));
|
DCHECK(!(FLAGS_output_media_info && mpd_notifier));
|
||||||
if (FLAGS_output_media_info) {
|
if (FLAGS_output_media_info) {
|
||||||
const std::string output_media_info_file_name =
|
const std::string output_media_info_file_name =
|
||||||
stream_muxer_options.output_file_name + ".media_info";
|
stream_muxer_options.output_file_name + kMediaInfoSuffix;
|
||||||
scoped_ptr<VodMediaInfoDumpMuxerListener>
|
scoped_ptr<VodMediaInfoDumpMuxerListener>
|
||||||
vod_media_info_dump_muxer_listener(
|
vod_media_info_dump_muxer_listener(
|
||||||
new VodMediaInfoDumpMuxerListener(output_media_info_file_name));
|
new VodMediaInfoDumpMuxerListener(output_media_info_file_name));
|
||||||
|
|
|
@ -24,7 +24,6 @@ struct MpdOptions;
|
||||||
namespace media {
|
namespace media {
|
||||||
|
|
||||||
class KeySource;
|
class KeySource;
|
||||||
class MediaInfo;
|
|
||||||
class MediaStream;
|
class MediaStream;
|
||||||
class Muxer;
|
class Muxer;
|
||||||
struct MuxerOptions;
|
struct MuxerOptions;
|
||||||
|
|
|
@ -28,6 +28,7 @@ class PackagerAppTest(unittest.TestCase):
|
||||||
self.tmp_dir = tempfile.mkdtemp()
|
self.tmp_dir = tempfile.mkdtemp()
|
||||||
self.output_prefix = os.path.join(self.tmp_dir, 'output')
|
self.output_prefix = os.path.join(self.tmp_dir, 'output')
|
||||||
self.mpd_output = self.output_prefix + '.mpd'
|
self.mpd_output = self.output_prefix + '.mpd'
|
||||||
|
self.output = None
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
shutil.rmtree(self.tmp_dir)
|
shutil.rmtree(self.tmp_dir)
|
||||||
|
@ -70,13 +71,30 @@ class PackagerAppTest(unittest.TestCase):
|
||||||
self._DiffGold(self.output[0], 'bear-640x360-v-golden.mp4')
|
self._DiffGold(self.output[0], 'bear-640x360-v-golden.mp4')
|
||||||
self._DiffGold(self.mpd_output, 'bear-640x360-v-golden.mpd')
|
self._DiffGold(self.mpd_output, 'bear-640x360-v-golden.mpd')
|
||||||
|
|
||||||
def testPackage(self):
|
def testPackageText(self):
|
||||||
self.packager.Package(self._GetStreams(['audio', 'video']),
|
self.packager.Package(self._GetStreams(['text'],
|
||||||
|
test_file='subtitle-english.vtt'),
|
||||||
self._GetFlags())
|
self._GetFlags())
|
||||||
|
self._DiffGold(self.output[0], 'subtitle-english-golden.vtt')
|
||||||
|
self._DiffGold(self.mpd_output, 'subtitle-english-vtt-golden.mpd')
|
||||||
|
|
||||||
|
# Probably one of the most common scenarios is to package audio and video.
|
||||||
|
def testPackageAudioVideo(self):
|
||||||
|
self.packager.Package(self._GetStreams(['audio', 'video']), self._GetFlags())
|
||||||
self._DiffGold(self.output[0], 'bear-640x360-a-golden.mp4')
|
self._DiffGold(self.output[0], 'bear-640x360-a-golden.mp4')
|
||||||
self._DiffGold(self.output[1], 'bear-640x360-v-golden.mp4')
|
self._DiffGold(self.output[1], 'bear-640x360-v-golden.mp4')
|
||||||
self._DiffGold(self.mpd_output, 'bear-640x360-av-golden.mpd')
|
self._DiffGold(self.mpd_output, 'bear-640x360-av-golden.mpd')
|
||||||
|
|
||||||
|
# Package all video, audio, and text.
|
||||||
|
def testPackageVideoAudioText(self):
|
||||||
|
audio_video_streams = self._GetStreams(['audio', 'video'])
|
||||||
|
text_stream = self._GetStreams(['text'], test_file='subtitle-english.vtt')
|
||||||
|
self.packager.Package(audio_video_streams + text_stream, self._GetFlags())
|
||||||
|
self._DiffGold(self.output[0], 'bear-640x360-a-golden.mp4')
|
||||||
|
self._DiffGold(self.output[1], 'bear-640x360-v-golden.mp4')
|
||||||
|
self._DiffGold(self.output[2], 'subtitle-english-golden.vtt')
|
||||||
|
self._DiffGold(self.mpd_output, 'bear-640x360-avt-golden.mpd')
|
||||||
|
|
||||||
def testPackageWithEncryption(self):
|
def testPackageWithEncryption(self):
|
||||||
self.packager.Package(self._GetStreams(['audio', 'video']),
|
self.packager.Package(self._GetStreams(['audio', 'video']),
|
||||||
self._GetFlags(encryption=True))
|
self._GetFlags(encryption=True))
|
||||||
|
@ -208,7 +226,8 @@ class PackagerAppTest(unittest.TestCase):
|
||||||
def _GetStreams(self, stream_descriptors, live = False,
|
def _GetStreams(self, stream_descriptors, live = False,
|
||||||
test_file = 'bear-640x360.mp4'):
|
test_file = 'bear-640x360.mp4'):
|
||||||
streams = []
|
streams = []
|
||||||
self.output = []
|
if not self.output:
|
||||||
|
self.output = []
|
||||||
|
|
||||||
test_data_dir = os.path.join(
|
test_data_dir = os.path.join(
|
||||||
test_env.SRC_DIR, 'packager', 'media', 'test', 'data')
|
test_env.SRC_DIR, 'packager', 'media', 'test', 'data')
|
||||||
|
@ -221,12 +240,20 @@ class PackagerAppTest(unittest.TestCase):
|
||||||
'segment_template=%s-$Number$.m4s')
|
'segment_template=%s-$Number$.m4s')
|
||||||
streams.append(stream % (input, stream_descriptor, output, output))
|
streams.append(stream % (input, stream_descriptor, output, output))
|
||||||
else:
|
else:
|
||||||
output = '%s_%s.mp4' % (self.output_prefix, stream_descriptor)
|
output = '%s_%s.%s' % (self.output_prefix,
|
||||||
|
stream_descriptor,
|
||||||
|
self._GetExtension(stream_descriptor))
|
||||||
stream = 'input=%s,stream=%s,output=%s'
|
stream = 'input=%s,stream=%s,output=%s'
|
||||||
streams.append(stream % (input, stream_descriptor, output))
|
streams.append(stream % (input, stream_descriptor, output))
|
||||||
self.output.append(output)
|
self.output.append(output)
|
||||||
return streams
|
return streams
|
||||||
|
|
||||||
|
def _GetExtension(self, stream_descriptor):
|
||||||
|
# TODO(rkuroiwa): Support ttml.
|
||||||
|
if stream_descriptor == "text":
|
||||||
|
return "vtt"
|
||||||
|
return "mp4"
|
||||||
|
|
||||||
def _GetFlags(self, encryption = False, random_iv = False,
|
def _GetFlags(self, encryption = False, random_iv = False,
|
||||||
widevine_encryption = False, key_rotation = False,
|
widevine_encryption = False, key_rotation = False,
|
||||||
live = False, dash_if_iop=False, output_media_info = False,
|
live = False, dash_if_iop=False, output_media_info = False,
|
||||||
|
@ -276,14 +303,14 @@ class PackagerAppTest(unittest.TestCase):
|
||||||
print "Updating golden file: ", golden_file_name
|
print "Updating golden file: ", golden_file_name
|
||||||
shutil.copyfile(test_output, golden_file)
|
shutil.copyfile(test_output, golden_file)
|
||||||
else:
|
else:
|
||||||
match = filecmp.cmp(test_output, golden_file)
|
match = filecmp.cmp(test_output, golden_file)
|
||||||
if not match:
|
if not match:
|
||||||
p = subprocess.Popen(['git', '--no-pager', 'diff', '--color=auto',
|
p = subprocess.Popen(['git', '--no-pager', 'diff', '--color=auto',
|
||||||
'--no-ext-diff', '--no-index',
|
'--no-ext-diff', '--no-index',
|
||||||
golden_file, test_output],
|
golden_file, test_output],
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
output, error = p.communicate()
|
output, error = p.communicate()
|
||||||
self.fail(output + error)
|
self.fail(output + error)
|
||||||
|
|
||||||
# '*.media_info' outputs contain media file names, which is changing for
|
# '*.media_info' outputs contain media file names, which is changing for
|
||||||
# every test run. These needs to be replaced for comparison.
|
# every test run. These needs to be replaced for comparison.
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<MPD xmlns="urn:mpeg:DASH:schema:MPD:2011" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd" xmlns:cenc="urn:mpeg:cenc:2013" minBufferTime="PT2S" type="static" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" mediaPresentationDuration="PT2.763174533843994S">
|
||||||
|
<Period>
|
||||||
|
<AdaptationSet id="0" contentType="text">
|
||||||
|
<Representation id="0" bandwidth="256" mimeType="text/vtt">
|
||||||
|
<BaseURL>output_text.vtt</BaseURL>
|
||||||
|
</Representation>
|
||||||
|
</AdaptationSet>
|
||||||
|
<AdaptationSet id="1" contentType="video" width="640" height="360" frameRate="30000/1001" subSegmentAlignment="true" par="16:9">
|
||||||
|
<Representation id="1" bandwidth="881637" codecs="avc1.64001e" mimeType="video/mp4" sar="1:1" width="640" height="360" frameRate="30000/1001">
|
||||||
|
<BaseURL>output_video.mp4</BaseURL>
|
||||||
|
<SegmentBase indexRange="677-744" timescale="30000">
|
||||||
|
<Initialization range="0-676"/>
|
||||||
|
</SegmentBase>
|
||||||
|
</Representation>
|
||||||
|
</AdaptationSet>
|
||||||
|
<AdaptationSet id="2" contentType="audio" subSegmentAlignment="true">
|
||||||
|
<Representation id="2" bandwidth="126087" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
|
||||||
|
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
|
||||||
|
<BaseURL>output_audio.mp4</BaseURL>
|
||||||
|
<SegmentBase indexRange="611-678" timescale="44100">
|
||||||
|
<Initialization range="0-610"/>
|
||||||
|
</SegmentBase>
|
||||||
|
</Representation>
|
||||||
|
</AdaptationSet>
|
||||||
|
</Period>
|
||||||
|
</MPD>
|
|
@ -0,0 +1,79 @@
|
||||||
|
WEBVTT FILE
|
||||||
|
|
||||||
|
1
|
||||||
|
00:00:03.837 --> 00:00:07.299
|
||||||
|
Captain's log, stardate 41636.9.
|
||||||
|
|
||||||
|
2
|
||||||
|
00:00:07.466 --> 00:00:11.845
|
||||||
|
As feared, our examination of the
|
||||||
|
overdue Federation freighter Odin,
|
||||||
|
|
||||||
|
3
|
||||||
|
00:00:12.012 --> 00:00:16.475
|
||||||
|
disabled by an asteroid collision,
|
||||||
|
revealed no life signs.
|
||||||
|
|
||||||
|
4
|
||||||
|
00:00:16.642 --> 00:00:19.019
|
||||||
|
However three escape pods
|
||||||
|
were missing,
|
||||||
|
|
||||||
|
5
|
||||||
|
00:00:19.186 --> 00:00:21.939
|
||||||
|
suggesting
|
||||||
|
the possibility of survivors.
|
||||||
|
|
||||||
|
6
|
||||||
|
00:00:22.606 --> 00:00:27.861
|
||||||
|
- Ready to orbit Angel One.
|
||||||
|
- What kind of place is this, Data?
|
||||||
|
|
||||||
|
7
|
||||||
|
00:00:28.028 --> 00:00:31.615
|
||||||
|
A Class-M planet supporting
|
||||||
|
carbon-based flora and fauna,
|
||||||
|
|
||||||
|
8
|
||||||
|
00:00:31.782 --> 00:00:34.326
|
||||||
|
sparsely populated
|
||||||
|
with intelligent life.
|
||||||
|
|
||||||
|
9
|
||||||
|
00:00:34.493 --> 00:00:38.497
|
||||||
|
Similar in technological development
|
||||||
|
to mid-20th century Earth.
|
||||||
|
|
||||||
|
10
|
||||||
|
00:00:38.664 --> 00:00:41.000
|
||||||
|
Kinda like being marooned at home.
|
||||||
|
|
||||||
|
11
|
||||||
|
00:00:41.166 --> 00:00:43.586
|
||||||
|
Assuming any survivors
|
||||||
|
made it this far.
|
||||||
|
|
||||||
|
12
|
||||||
|
00:00:43.794 --> 00:00:49.174
|
||||||
|
It is the closest planet, but to
|
||||||
|
go the distance we did in two days,
|
||||||
|
|
||||||
|
13
|
||||||
|
00:00:49.341 --> 00:00:52.344
|
||||||
|
would've taken the Odin escape pod
|
||||||
|
five months.
|
||||||
|
|
||||||
|
14
|
||||||
|
00:00:52.511 --> 00:00:54.680
|
||||||
|
Five months, six days, 11 hours,
|
||||||
|
two min...
|
||||||
|
|
||||||
|
15
|
||||||
|
00:00:54.847 --> 00:00:58.392
|
||||||
|
- Thank you, Data.
|
||||||
|
- ...and 57 seconds.
|
||||||
|
|
||||||
|
16
|
||||||
|
00:00:58.559 --> 00:01:01.353
|
||||||
|
Receiving an audio signal
|
||||||
|
from Angel One.
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<MPD xmlns="urn:mpeg:DASH:schema:MPD:2011" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd" xmlns:cenc="urn:mpeg:cenc:2013" minBufferTime="PT2S" type="static" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" mediaPresentationDuration="PT0S">
|
||||||
|
<Period>
|
||||||
|
<AdaptationSet id="0" contentType="text">
|
||||||
|
<Representation id="0" bandwidth="256" mimeType="text/vtt">
|
||||||
|
<BaseURL>output_text.vtt</BaseURL>
|
||||||
|
</Representation>
|
||||||
|
</AdaptationSet>
|
||||||
|
</Period>
|
||||||
|
</MPD>
|
|
@ -1618,6 +1618,29 @@ static MediaContainerName LookupContainerByFirst4(const uint8_t* buffer,
|
||||||
return CONTAINER_UNKNOWN;
|
return CONTAINER_UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
const char kWebVtt[] = "WEBVTT";
|
||||||
|
|
||||||
|
bool CheckWebVtt(const uint8_t* buffer, int buffer_size) {
|
||||||
|
const int offset =
|
||||||
|
StartsWith(buffer, buffer_size, UTF8_BYTE_ORDER_MARK) ? 3 : 0;
|
||||||
|
|
||||||
|
return StartsWith(buffer + offset, buffer_size - offset,
|
||||||
|
reinterpret_cast<const uint8_t*>(kWebVtt),
|
||||||
|
arraysize(kWebVtt) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(rkuroiwa): This check is a very simple check to see if it is UTF-8 or
|
||||||
|
// UTF-16, which is not sufficient to determine whether it is TTML. Check if the
|
||||||
|
// entire buffer is a valid TTML.
|
||||||
|
bool CheckTtml(const uint8_t* buffer, int buffer_size) {
|
||||||
|
return StartsWith(buffer, buffer_size,
|
||||||
|
"<?xml version='1.0' encoding='UTF-8'?>") ||
|
||||||
|
StartsWith(buffer, buffer_size,
|
||||||
|
"<?xml version='1.0' encoding='UTF-16'?>");
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
// Attempt to determine the container name from the buffer provided.
|
// Attempt to determine the container name from the buffer provided.
|
||||||
MediaContainerName DetermineContainer(const uint8_t* buffer, int buffer_size) {
|
MediaContainerName DetermineContainer(const uint8_t* buffer, int buffer_size) {
|
||||||
DCHECK(buffer);
|
DCHECK(buffer);
|
||||||
|
@ -1632,6 +1655,10 @@ MediaContainerName DetermineContainer(const uint8_t* buffer, int buffer_size) {
|
||||||
if (result != CONTAINER_UNKNOWN)
|
if (result != CONTAINER_UNKNOWN)
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
|
// WebVTT check only checks for the first few bytes.
|
||||||
|
if (CheckWebVtt(buffer, buffer_size))
|
||||||
|
return CONTAINER_WEBVTT;
|
||||||
|
|
||||||
// Additional checks that may scan a portion of the buffer.
|
// Additional checks that may scan a portion of the buffer.
|
||||||
if (CheckMpeg2ProgramStream(buffer, buffer_size))
|
if (CheckMpeg2ProgramStream(buffer, buffer_size))
|
||||||
return CONTAINER_MPEG2PS;
|
return CONTAINER_MPEG2PS;
|
||||||
|
@ -1666,6 +1693,11 @@ MediaContainerName DetermineContainer(const uint8_t* buffer, int buffer_size) {
|
||||||
return CONTAINER_EAC3;
|
return CONTAINER_EAC3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// To do a TTML check, it (should) do a schema check which requires scanning
|
||||||
|
// the whole content.
|
||||||
|
if (CheckTtml(buffer, buffer_size))
|
||||||
|
return CONTAINER_TTML;
|
||||||
|
|
||||||
return CONTAINER_UNKNOWN;
|
return CONTAINER_UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,9 +47,11 @@ enum MediaContainerName {
|
||||||
CONTAINER_RM, // RM (RealMedia)
|
CONTAINER_RM, // RM (RealMedia)
|
||||||
CONTAINER_SRT, // SRT (SubRip subtitle)
|
CONTAINER_SRT, // SRT (SubRip subtitle)
|
||||||
CONTAINER_SWF, // SWF (ShockWave Flash)
|
CONTAINER_SWF, // SWF (ShockWave Flash)
|
||||||
|
CONTAINER_TTML, // TTML file.
|
||||||
CONTAINER_VC1, // VC-1
|
CONTAINER_VC1, // VC-1
|
||||||
CONTAINER_WAV, // WAV / WAVE (Waveform Audio)
|
CONTAINER_WAV, // WAV / WAVE (Waveform Audio)
|
||||||
CONTAINER_WEBM, // Matroska / WebM
|
CONTAINER_WEBM, // Matroska / WebM
|
||||||
|
CONTAINER_WEBVTT, // WebVTT file.
|
||||||
CONTAINER_WTV, // WTV (Windows Television)
|
CONTAINER_WTV, // WTV (Windows Television)
|
||||||
CONTAINER_MAX // Must be last
|
CONTAINER_MAX // Must be last
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,8 +19,8 @@ namespace edash_packager {
|
||||||
namespace media {
|
namespace media {
|
||||||
|
|
||||||
VodMediaInfoDumpMuxerListener::VodMediaInfoDumpMuxerListener(
|
VodMediaInfoDumpMuxerListener::VodMediaInfoDumpMuxerListener(
|
||||||
const std::string& output_file_name)
|
const std::string& output_file_path)
|
||||||
: output_file_name_(output_file_name), is_encrypted_(false) {}
|
: output_file_name_(output_file_path), is_encrypted_(false) {}
|
||||||
|
|
||||||
VodMediaInfoDumpMuxerListener::~VodMediaInfoDumpMuxerListener() {}
|
VodMediaInfoDumpMuxerListener::~VodMediaInfoDumpMuxerListener() {}
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ void VodMediaInfoDumpMuxerListener::OnMediaEnd(bool has_init_range,
|
||||||
LOG(ERROR) << "Failed to generate VOD information from input.";
|
LOG(ERROR) << "Failed to generate VOD information from input.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SerializeMediaInfoToFile();
|
WriteMediaInfoToFile(*media_info_, output_file_name_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void VodMediaInfoDumpMuxerListener::OnNewSegment(uint64_t start_time,
|
void VodMediaInfoDumpMuxerListener::OnNewSegment(uint64_t start_time,
|
||||||
|
@ -105,17 +105,20 @@ void VodMediaInfoDumpMuxerListener::OnNewSegment(uint64_t start_time,
|
||||||
uint64_t segment_file_size) {
|
uint64_t segment_file_size) {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VodMediaInfoDumpMuxerListener::SerializeMediaInfoToFile() {
|
// static
|
||||||
|
bool VodMediaInfoDumpMuxerListener::WriteMediaInfoToFile(
|
||||||
|
const edash_packager::MediaInfo& media_info,
|
||||||
|
const std::string& output_file_path) {
|
||||||
std::string output_string;
|
std::string output_string;
|
||||||
if (!google::protobuf::TextFormat::PrintToString(*media_info_,
|
if (!google::protobuf::TextFormat::PrintToString(media_info,
|
||||||
&output_string)) {
|
&output_string)) {
|
||||||
LOG(ERROR) << "Failed to serialize MediaInfo to string.";
|
LOG(ERROR) << "Failed to serialize MediaInfo to string.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
media::File* file = File::Open(output_file_name_.c_str(), "w");
|
media::File* file = File::Open(output_file_path.c_str(), "w");
|
||||||
if (!file) {
|
if (!file) {
|
||||||
LOG(ERROR) << "Failed to open " << output_file_name_;
|
LOG(ERROR) << "Failed to open " << output_file_path;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (file->Write(output_string.data(), output_string.size()) <= 0) {
|
if (file->Write(output_string.data(), output_string.size()) <= 0) {
|
||||||
|
@ -124,7 +127,7 @@ bool VodMediaInfoDumpMuxerListener::SerializeMediaInfoToFile() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!file->Close()) {
|
if (!file->Close()) {
|
||||||
LOG(ERROR) << "Failed to close " << output_file_name_;
|
LOG(ERROR) << "Failed to close " << output_file_path;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -59,9 +59,16 @@ class VodMediaInfoDumpMuxerListener : public MuxerListener {
|
||||||
uint64_t segment_file_size) override;
|
uint64_t segment_file_size) override;
|
||||||
/// @}
|
/// @}
|
||||||
|
|
||||||
|
/// Write @a media_info to @a output_file_path in human readable format.
|
||||||
|
/// @param media_info is the MediaInfo to write out.
|
||||||
|
/// @param output_file_path is the path of the output file.
|
||||||
|
/// @return true on success, false otherwise.
|
||||||
|
// TODO(rkuroiwa): Move this to muxer_listener_internal and rename
|
||||||
|
// muxer_listener_internal to muxer_listener_util.
|
||||||
|
static bool WriteMediaInfoToFile(const MediaInfo& media_info,
|
||||||
|
const std::string& output_file_path);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Write |media_info_| to |output_file_name_|.
|
|
||||||
bool SerializeMediaInfoToFile();
|
|
||||||
|
|
||||||
std::string output_file_name_;
|
std::string output_file_name_;
|
||||||
std::string scheme_id_uri_;
|
std::string scheme_id_uri_;
|
||||||
|
|
|
@ -175,5 +175,35 @@ bool File::ReadFileToString(const char* file_name, std::string* contents) {
|
||||||
return len == 0;
|
return len == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool File::Copy(const char* from_file_name, const char* to_file_name) {
|
||||||
|
std::string content;
|
||||||
|
if (!ReadFileToString(from_file_name, &content)) {
|
||||||
|
LOG(ERROR) << "Failed to open file " << from_file_name;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
scoped_ptr<edash_packager::media::File, edash_packager::media::FileCloser>
|
||||||
|
output_file(edash_packager::media::File::Open(to_file_name, "w"));
|
||||||
|
if (!output_file) {
|
||||||
|
LOG(ERROR) << "Failed to write to " << to_file_name;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t bytes_left = content.size();
|
||||||
|
uint64_t total_bytes_written = 0;
|
||||||
|
const char* content_cstr = content.c_str();
|
||||||
|
while (bytes_left > total_bytes_written) {
|
||||||
|
const int64_t bytes_written =
|
||||||
|
output_file->Write(content_cstr + total_bytes_written, bytes_left);
|
||||||
|
if (bytes_written < 0) {
|
||||||
|
LOG(ERROR) << "Failure while writing to " << to_file_name;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
total_bytes_written += bytes_written;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace media
|
} // namespace media
|
||||||
} // namespace edash_packager
|
} // namespace edash_packager
|
||||||
|
|
|
@ -102,6 +102,14 @@ class File {
|
||||||
/// @return true on success, false otherwise.
|
/// @return true on success, false otherwise.
|
||||||
static bool ReadFileToString(const char* file_name, std::string* contents);
|
static bool ReadFileToString(const char* file_name, std::string* contents);
|
||||||
|
|
||||||
|
/// Copies files. This is not good for copying huge files. Although not
|
||||||
|
/// recommended, it is safe to have source file and destination file name be
|
||||||
|
/// the same.
|
||||||
|
/// @param from_file_name is the source file name.
|
||||||
|
/// @param to_file_name is the destination file name.
|
||||||
|
/// @return true on success, false otherwise.
|
||||||
|
static bool Copy(const char* from_file_name, const char* to_file_name);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
explicit File(const std::string& file_name) : file_name_(file_name) {}
|
explicit File(const std::string& file_name) : file_name_(file_name) {}
|
||||||
/// Do *not* call the destructor directly (with the "delete" keyword)
|
/// Do *not* call the destructor directly (with the "delete" keyword)
|
||||||
|
|
|
@ -60,6 +60,30 @@ TEST_F(LocalFileTest, Size) {
|
||||||
ASSERT_EQ(kDataSize, File::GetFileSize(local_file_name_.c_str()));
|
ASSERT_EQ(kDataSize, File::GetFileSize(local_file_name_.c_str()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(LocalFileTest, Copy) {
|
||||||
|
ASSERT_EQ(kDataSize,
|
||||||
|
base::WriteFile(test_file_path_, data_.data(), kDataSize));
|
||||||
|
|
||||||
|
base::FilePath temp_dir;
|
||||||
|
ASSERT_TRUE(base::CreateNewTempDirectory("", &temp_dir));
|
||||||
|
|
||||||
|
// Copy the test file to temp dir as filename "a".
|
||||||
|
base::FilePath destination = temp_dir.Append("a");
|
||||||
|
ASSERT_TRUE(
|
||||||
|
File::Copy(local_file_name_.c_str(), destination.value().c_str()));
|
||||||
|
|
||||||
|
// Make a buffer bigger than the expected file content size to make sure that
|
||||||
|
// there isn't extra stuff appended.
|
||||||
|
char copied_file_content_buffer[kDataSize * 2] = {};
|
||||||
|
ASSERT_EQ(kDataSize, base::ReadFile(destination,
|
||||||
|
copied_file_content_buffer,
|
||||||
|
arraysize(copied_file_content_buffer)));
|
||||||
|
|
||||||
|
ASSERT_EQ(data_, std::string(copied_file_content_buffer, kDataSize));
|
||||||
|
|
||||||
|
base::DeleteFile(temp_dir, true);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(LocalFileTest, Write) {
|
TEST_F(LocalFileTest, Write) {
|
||||||
// Write file using File API.
|
// Write file using File API.
|
||||||
File* file = File::Open(local_file_name_.c_str(), "w");
|
File* file = File::Open(local_file_name_.c_str(), "w");
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
WEBVTT FILE
|
||||||
|
|
||||||
|
1
|
||||||
|
00:00:03.837 --> 00:00:07.299
|
||||||
|
Captain's log, stardate 41636.9.
|
||||||
|
|
||||||
|
2
|
||||||
|
00:00:07.466 --> 00:00:11.845
|
||||||
|
As feared, our examination of the
|
||||||
|
overdue Federation freighter Odin,
|
||||||
|
|
||||||
|
3
|
||||||
|
00:00:12.012 --> 00:00:16.475
|
||||||
|
disabled by an asteroid collision,
|
||||||
|
revealed no life signs.
|
||||||
|
|
||||||
|
4
|
||||||
|
00:00:16.642 --> 00:00:19.019
|
||||||
|
However three escape pods
|
||||||
|
were missing,
|
||||||
|
|
||||||
|
5
|
||||||
|
00:00:19.186 --> 00:00:21.939
|
||||||
|
suggesting
|
||||||
|
the possibility of survivors.
|
||||||
|
|
||||||
|
6
|
||||||
|
00:00:22.606 --> 00:00:27.861
|
||||||
|
- Ready to orbit Angel One.
|
||||||
|
- What kind of place is this, Data?
|
||||||
|
|
||||||
|
7
|
||||||
|
00:00:28.028 --> 00:00:31.615
|
||||||
|
A Class-M planet supporting
|
||||||
|
carbon-based flora and fauna,
|
||||||
|
|
||||||
|
8
|
||||||
|
00:00:31.782 --> 00:00:34.326
|
||||||
|
sparsely populated
|
||||||
|
with intelligent life.
|
||||||
|
|
||||||
|
9
|
||||||
|
00:00:34.493 --> 00:00:38.497
|
||||||
|
Similar in technological development
|
||||||
|
to mid-20th century Earth.
|
||||||
|
|
||||||
|
10
|
||||||
|
00:00:38.664 --> 00:00:41.000
|
||||||
|
Kinda like being marooned at home.
|
||||||
|
|
||||||
|
11
|
||||||
|
00:00:41.166 --> 00:00:43.586
|
||||||
|
Assuming any survivors
|
||||||
|
made it this far.
|
||||||
|
|
||||||
|
12
|
||||||
|
00:00:43.794 --> 00:00:49.174
|
||||||
|
It is the closest planet, but to
|
||||||
|
go the distance we did in two days,
|
||||||
|
|
||||||
|
13
|
||||||
|
00:00:49.341 --> 00:00:52.344
|
||||||
|
would've taken the Odin escape pod
|
||||||
|
five months.
|
||||||
|
|
||||||
|
14
|
||||||
|
00:00:52.511 --> 00:00:54.680
|
||||||
|
Five months, six days, 11 hours,
|
||||||
|
two min...
|
||||||
|
|
||||||
|
15
|
||||||
|
00:00:54.847 --> 00:00:58.392
|
||||||
|
- Thank you, Data.
|
||||||
|
- ...and 57 seconds.
|
||||||
|
|
||||||
|
16
|
||||||
|
00:00:58.559 --> 00:01:01.353
|
||||||
|
Receiving an audio signal
|
||||||
|
from Angel One.
|
Loading…
Reference in New Issue