feat: Add support for single file TS for HLS (#934)
This is based on comments at https://github.com/google/shaka-packager/pull/891. The muxer is deciding whether to write to a single file or a segment file based on the configuration. Example: ``` ../packager 'in=TOS.ts,stream=video,output=tos_video.ts,playlist_name=tos_video.m3u8' \ 'in=TOS.ts,stream=audio,output=tos_audio.ts,playlist_name=tos_audio.m3u8' \ --hls_master_playlist_output tos.m3u8 ``` Tested the content using Exoplayer. --------- Co-authored-by: Cosmin Stejerean <cstejerean@meta.com>
This commit is contained in:
parent
6acdcc394a
commit
4aa4b4b9aa
|
@ -1597,6 +1597,13 @@ class PackagerFunctionalTest(PackagerAppTest):
|
|||
self._GetFlags(encryption=True, output_hls=True))
|
||||
self._CheckTestResults('ec3-and-hls-single-segment-mp4-encrypted')
|
||||
|
||||
def testHlsSingleSegmentTs(self):
|
||||
self.assertPackageSuccess(
|
||||
self._GetStreams(
|
||||
['audio', 'video'], hls=True, test_files=['bear-640x360.ts']),
|
||||
self._GetFlags(output_hls=True))
|
||||
self._CheckTestResults('hls-single-segment-ts')
|
||||
|
||||
def testEc3PackedAudioEncrypted(self):
|
||||
streams = [
|
||||
self._GetStream(
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
#EXTM3U
|
||||
#EXT-X-VERSION:6
|
||||
## Generated with https://github.com/shaka-project/shaka-packager version <tag>-<hash>-<test>
|
||||
#EXT-X-TARGETDURATION:2
|
||||
#EXT-X-PLAYLIST-TYPE:VOD
|
||||
#EXTINF:0.975,
|
||||
#EXT-X-BYTERANGE:23312@0
|
||||
bear-640x360-audio.ts
|
||||
#EXTINF:0.998,
|
||||
#EXT-X-BYTERANGE:24252
|
||||
bear-640x360-audio.ts
|
||||
#EXTINF:0.789,
|
||||
#EXT-X-BYTERANGE:17296
|
||||
bear-640x360-audio.ts
|
||||
#EXT-X-ENDLIST
|
Binary file not shown.
16
packager/app/test/testdata/hls-single-segment-ts/bear-640x360-video-iframe.m3u8
vendored
Normal file
16
packager/app/test/testdata/hls-single-segment-ts/bear-640x360-video-iframe.m3u8
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
#EXTM3U
|
||||
#EXT-X-VERSION:6
|
||||
## Generated with https://github.com/shaka-project/shaka-packager version <tag>-<hash>-<test>
|
||||
#EXT-X-TARGETDURATION:2
|
||||
#EXT-X-PLAYLIST-TYPE:VOD
|
||||
#EXT-X-I-FRAMES-ONLY
|
||||
#EXTINF:1.001,
|
||||
#EXT-X-BYTERANGE:15604@376
|
||||
bear-640x360-video.ts
|
||||
#EXTINF:1.001,
|
||||
#EXT-X-BYTERANGE:18236@105656
|
||||
bear-640x360-video.ts
|
||||
#EXTINF:0.734,
|
||||
#EXT-X-BYTERANGE:19928@233684
|
||||
bear-640x360-video.ts
|
||||
#EXT-X-ENDLIST
|
|
@ -0,0 +1,15 @@
|
|||
#EXTM3U
|
||||
#EXT-X-VERSION:6
|
||||
## Generated with https://github.com/shaka-project/shaka-packager version <tag>-<hash>-<test>
|
||||
#EXT-X-TARGETDURATION:2
|
||||
#EXT-X-PLAYLIST-TYPE:VOD
|
||||
#EXTINF:1.001,
|
||||
#EXT-X-BYTERANGE:105280@0
|
||||
bear-640x360-video.ts
|
||||
#EXTINF:1.001,
|
||||
#EXT-X-BYTERANGE:128028
|
||||
bear-640x360-video.ts
|
||||
#EXTINF:0.734,
|
||||
#EXT-X-BYTERANGE:84600
|
||||
bear-640x360-video.ts
|
||||
#EXT-X-ENDLIST
|
Binary file not shown.
|
@ -0,0 +1,11 @@
|
|||
#EXTM3U
|
||||
## Generated with https://github.com/shaka-project/shaka-packager version <tag>-<hash>-<test>
|
||||
|
||||
#EXT-X-INDEPENDENT-SEGMENTS
|
||||
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,URI="bear-640x360-audio.m3u8",GROUP-ID="default-audio-group",NAME="stream_0",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="2"
|
||||
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=1217520,AVERAGE-BANDWIDTH=1117320,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=29.970,AUDIO="default-audio-group",CLOSED-CAPTIONS=NONE
|
||||
bear-640x360-video.m3u8
|
||||
|
||||
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=217180,AVERAGE-BANDWIDTH=157213,CODECS="avc1.64001e",RESOLUTION=640x360,CLOSED-CAPTIONS=NONE,URI="bear-640x360-video-iframe.m3u8"
|
|
@ -8,6 +8,9 @@
|
|||
|
||||
#include <absl/log/check.h>
|
||||
|
||||
#include <packager/macros/status.h>
|
||||
#include <packager/media/base/muxer_util.h>
|
||||
|
||||
namespace shaka {
|
||||
namespace media {
|
||||
namespace mp2t {
|
||||
|
@ -23,6 +26,16 @@ Status TsMuxer::InitializeMuxer() {
|
|||
if (streams().size() > 1u)
|
||||
return Status(error::MUXER_FAILURE, "Cannot handle more than one streams.");
|
||||
|
||||
if (options().segment_template.empty()) {
|
||||
const std::string& file_name = options().output_file_name;
|
||||
DCHECK(!file_name.empty());
|
||||
output_file_.reset(File::Open(file_name.c_str(), "w"));
|
||||
if (!output_file_) {
|
||||
return Status(error::FILE_FAILURE,
|
||||
"Cannot open file for write " + file_name);
|
||||
}
|
||||
}
|
||||
|
||||
segmenter_.reset(new TsSegmenter(options(), muxer_listener()));
|
||||
Status status = segmenter_->Initialize(*streams()[0]);
|
||||
FireOnMediaStartEvent();
|
||||
|
@ -49,10 +62,81 @@ Status TsMuxer::AddMediaSample(size_t stream_id, const MediaSample& sample) {
|
|||
Status TsMuxer::FinalizeSegment(size_t stream_id,
|
||||
const SegmentInfo& segment_info) {
|
||||
DCHECK_EQ(stream_id, 0u);
|
||||
return segment_info.is_subsegment
|
||||
? Status::OK
|
||||
: segmenter_->FinalizeSegment(segment_info.start_timestamp,
|
||||
segment_info.duration);
|
||||
|
||||
if (segment_info.is_subsegment)
|
||||
return Status::OK;
|
||||
|
||||
Status s = segmenter_->FinalizeSegment(segment_info.start_timestamp,
|
||||
segment_info.duration);
|
||||
if (!s.ok())
|
||||
return s;
|
||||
if (!segmenter_->segment_started())
|
||||
return Status::OK;
|
||||
|
||||
int64_t segment_start_timestamp = segmenter_->segment_start_timestamp();
|
||||
|
||||
std::string segment_path =
|
||||
options().segment_template.empty()
|
||||
? options().output_file_name
|
||||
: GetSegmentName(options().segment_template, segment_start_timestamp,
|
||||
segment_number_++, options().bandwidth);
|
||||
|
||||
const int64_t file_size = segmenter_->segment_buffer()->Size();
|
||||
|
||||
RETURN_IF_ERROR(WriteSegment(segment_path, segmenter_->segment_buffer()));
|
||||
|
||||
total_duration_ += segment_info.duration;
|
||||
|
||||
if (muxer_listener()) {
|
||||
muxer_listener()->OnNewSegment(
|
||||
segment_path,
|
||||
segment_info.start_timestamp * segmenter_->timescale() +
|
||||
segmenter_->transport_stream_timestamp_offset(),
|
||||
segment_info.duration * segmenter_->timescale(), file_size);
|
||||
}
|
||||
|
||||
segmenter_->set_segment_started(false);
|
||||
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
Status TsMuxer::WriteSegment(const std::string& segment_path,
|
||||
BufferWriter* segment_buffer) {
|
||||
std::unique_ptr<File, FileCloser> file;
|
||||
|
||||
if (output_file_) {
|
||||
// This is in single segment mode.
|
||||
Range range;
|
||||
range.start = media_ranges_.subsegment_ranges.empty()
|
||||
? 0
|
||||
: (media_ranges_.subsegment_ranges.back().end + 1);
|
||||
range.end = range.start + segment_buffer->Size() - 1;
|
||||
media_ranges_.subsegment_ranges.push_back(range);
|
||||
} else {
|
||||
file.reset(File::Open(segment_path.c_str(), "w"));
|
||||
if (!file) {
|
||||
return Status(error::FILE_FAILURE,
|
||||
"Cannot open file for write " + segment_path);
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_IF_ERROR(segment_buffer->WriteToFile(output_file_ ? output_file_.get()
|
||||
: file.get()));
|
||||
|
||||
if (file)
|
||||
RETURN_IF_ERROR(CloseFile(std::move(file)));
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
Status TsMuxer::CloseFile(std::unique_ptr<File, FileCloser> file) {
|
||||
std::string file_name = file->file_name();
|
||||
if (!file.release()->Close()) {
|
||||
return Status(
|
||||
error::FILE_FAILURE,
|
||||
"Cannot close file " + file_name +
|
||||
", possibly file permission issue or running out of disk space.");
|
||||
}
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
void TsMuxer::FireOnMediaStartEvent() {
|
||||
|
@ -66,10 +150,7 @@ void TsMuxer::FireOnMediaEndEvent() {
|
|||
if (!muxer_listener())
|
||||
return;
|
||||
|
||||
// For now, there is no single file TS segmenter. So all the values passed
|
||||
// here are left empty.
|
||||
MuxerListener::MediaRanges range;
|
||||
muxer_listener()->OnMediaEnd(range, 0);
|
||||
muxer_listener()->OnMediaEnd(media_ranges_, total_duration_);
|
||||
}
|
||||
|
||||
} // namespace mp2t
|
||||
|
|
|
@ -30,6 +30,10 @@ class TsMuxer : public Muxer {
|
|||
Status FinalizeSegment(size_t stream_id,
|
||||
const SegmentInfo& sample) override;
|
||||
|
||||
Status WriteSegment(const std::string& segment_path,
|
||||
BufferWriter* segment_buffer);
|
||||
Status CloseFile(std::unique_ptr<File, FileCloser> file);
|
||||
|
||||
void FireOnMediaStartEvent();
|
||||
void FireOnMediaEndEvent();
|
||||
|
||||
|
@ -37,6 +41,17 @@ class TsMuxer : public Muxer {
|
|||
int64_t sample_durations_[2];
|
||||
int64_t num_samples_ = 0;
|
||||
|
||||
// Used in multi-segment mode for segment template.
|
||||
uint64_t segment_number_ = 0;
|
||||
|
||||
// Used in single segment mode.
|
||||
std::unique_ptr<File, FileCloser> output_file_;
|
||||
|
||||
// Keeps track of segment ranges in single segment mode.
|
||||
MuxerListener::MediaRanges media_ranges_;
|
||||
|
||||
uint64_t total_duration_ = 0;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(TsMuxer);
|
||||
};
|
||||
|
||||
|
|
|
@ -37,8 +37,7 @@ bool IsVideoCodec(Codec codec) {
|
|||
} // namespace
|
||||
|
||||
TsSegmenter::TsSegmenter(const MuxerOptions& options, MuxerListener* listener)
|
||||
: muxer_options_(options),
|
||||
listener_(listener),
|
||||
: listener_(listener),
|
||||
transport_stream_timestamp_offset_(
|
||||
options.transport_stream_timestamp_offset_ms * kTsTimescale / 1000),
|
||||
pes_packet_generator_(
|
||||
|
@ -47,8 +46,6 @@ TsSegmenter::TsSegmenter(const MuxerOptions& options, MuxerListener* listener)
|
|||
TsSegmenter::~TsSegmenter() {}
|
||||
|
||||
Status TsSegmenter::Initialize(const StreamInfo& stream_info) {
|
||||
if (muxer_options_.segment_template.empty())
|
||||
return Status(error::MUXER_FAILURE, "Segment template not specified.");
|
||||
if (!pes_packet_generator_->Initialize(stream_info)) {
|
||||
return Status(error::MUXER_FAILURE,
|
||||
"Failed to initialize PesPacketGenerator.");
|
||||
|
@ -172,40 +169,6 @@ Status TsSegmenter::FinalizeSegment(int64_t start_timestamp, int64_t duration) {
|
|||
Status status = WritePesPackets();
|
||||
if (!status.ok())
|
||||
return status;
|
||||
|
||||
// This method may be called from Finalize() so segment_started_ could
|
||||
// be false.
|
||||
if (!segment_started_)
|
||||
return Status::OK;
|
||||
std::string segment_path =
|
||||
GetSegmentName(muxer_options_.segment_template, segment_start_timestamp_,
|
||||
segment_number_++, muxer_options_.bandwidth);
|
||||
|
||||
const int64_t file_size = segment_buffer_.Size();
|
||||
std::unique_ptr<File, FileCloser> segment_file;
|
||||
segment_file.reset(File::Open(segment_path.c_str(), "w"));
|
||||
if (!segment_file) {
|
||||
return Status(error::FILE_FAILURE,
|
||||
"Cannot open file for write " + segment_path);
|
||||
}
|
||||
|
||||
RETURN_IF_ERROR(segment_buffer_.WriteToFile(segment_file.get()));
|
||||
|
||||
if (!segment_file.release()->Close()) {
|
||||
return Status(
|
||||
error::FILE_FAILURE,
|
||||
"Cannot close file " + segment_path +
|
||||
", possibly file permission issue or running out of disk space.");
|
||||
}
|
||||
|
||||
if (listener_) {
|
||||
listener_->OnNewSegment(segment_path,
|
||||
start_timestamp * timescale_scale_ +
|
||||
transport_stream_timestamp_offset_,
|
||||
duration * timescale_scale_, file_size);
|
||||
}
|
||||
segment_started_ = false;
|
||||
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,9 +24,6 @@ class MuxerListener;
|
|||
|
||||
namespace mp2t {
|
||||
|
||||
// TODO(rkuroiwa): For now, this implements multifile segmenter. Like other
|
||||
// make this an abstract super class and implement multifile and single file
|
||||
// segmenters.
|
||||
class TsSegmenter {
|
||||
public:
|
||||
// TODO(rkuroiwa): Add progress listener?
|
||||
|
@ -72,13 +69,22 @@ class TsSegmenter {
|
|||
/// Only for testing.
|
||||
void SetSegmentStartedForTesting(bool value);
|
||||
|
||||
int64_t segment_start_timestamp() const { return segment_start_timestamp_; }
|
||||
BufferWriter* segment_buffer() { return &segment_buffer_; }
|
||||
void set_segment_started(bool value) { segment_started_ = value; }
|
||||
bool segment_started() const { return segment_started_; }
|
||||
|
||||
double timescale() const { return timescale_scale_; }
|
||||
uint32_t transport_stream_timestamp_offset() const {
|
||||
return transport_stream_timestamp_offset_;
|
||||
}
|
||||
|
||||
private:
|
||||
Status StartSegmentIfNeeded(int64_t next_pts);
|
||||
|
||||
// Writes PES packets (carried in TsPackets) to a buffer.
|
||||
Status WritePesPackets();
|
||||
|
||||
const MuxerOptions& muxer_options_;
|
||||
MuxerListener* const listener_;
|
||||
|
||||
// Codec for the stream.
|
||||
|
@ -87,18 +93,16 @@ class TsSegmenter {
|
|||
|
||||
const int32_t transport_stream_timestamp_offset_ = 0;
|
||||
// Scale used to scale the input stream to TS's timesccale (which is 90000).
|
||||
|
||||
// Used for calculating the duration in seconds fo the current segment.
|
||||
double timescale_scale_ = 1.0;
|
||||
|
||||
// Used for segment template.
|
||||
uint64_t segment_number_ = 0;
|
||||
|
||||
std::unique_ptr<TsWriter> ts_writer_;
|
||||
|
||||
BufferWriter segment_buffer_;
|
||||
|
||||
// Set to true if segment_buffer_ is initialized, set to false after
|
||||
// FinalizeSegment() succeeds.
|
||||
// FinalizeSegment() succeeds in ts_muxer.
|
||||
bool segment_started_ = false;
|
||||
std::unique_ptr<PesPacketGenerator> pes_packet_generator_;
|
||||
|
||||
|
|
|
@ -207,11 +207,6 @@ TEST_F(TsSegmenterTest, PassedSegmentDuration) {
|
|||
// Doesn't really matter how long this is.
|
||||
sample2->set_duration(kInputTimescale * 7);
|
||||
|
||||
EXPECT_CALL(mock_listener,
|
||||
OnNewSegment("memory://file1.ts",
|
||||
kFirstPts * kTimeScale / kInputTimescale,
|
||||
kTimeScale * 11, _));
|
||||
|
||||
Sequence writer_sequence;
|
||||
EXPECT_CALL(*mock_ts_writer_, NewSegment(_))
|
||||
.InSequence(writer_sequence)
|
||||
|
@ -245,10 +240,6 @@ TEST_F(TsSegmenterTest, PassedSegmentDuration) {
|
|||
EXPECT_CALL(*mock_pes_packet_generator_, Flush())
|
||||
.WillOnce(Return(true));
|
||||
|
||||
EXPECT_CALL(*mock_ts_writer_, NewSegment(_))
|
||||
.InSequence(writer_sequence)
|
||||
.WillOnce(Return(true));
|
||||
|
||||
EXPECT_CALL(*mock_ts_writer_, AddPesPacketMock(_, _))
|
||||
.Times(2)
|
||||
.WillRepeatedly(Return(true));
|
||||
|
@ -391,8 +382,6 @@ TEST_F(TsSegmenterTest, EncryptedSample) {
|
|||
.InSequence(pes_packet_sequence)
|
||||
.WillOnce(Return(new PesPacket()));
|
||||
|
||||
EXPECT_CALL(mock_listener, OnNewSegment("memory://file1.ts", _, _, _));
|
||||
|
||||
MockTsWriter* mock_ts_writer_raw = mock_ts_writer_.get();
|
||||
|
||||
segmenter.InjectPesPacketGeneratorForTesting(
|
||||
|
|
|
@ -224,33 +224,17 @@ Status ValidateStreamDescriptor(bool dump_stream_info,
|
|||
if (output_format == CONTAINER_UNKNOWN) {
|
||||
return Status(error::INVALID_ARGUMENT, "Unsupported output format.");
|
||||
}
|
||||
if (output_format == MediaContainerName::CONTAINER_MPEG2TS) {
|
||||
if (stream.segment_template.empty()) {
|
||||
return Status(
|
||||
error::INVALID_ARGUMENT,
|
||||
"Please specify 'segment_template'. Single file TS output is "
|
||||
"not supported.");
|
||||
}
|
||||
|
||||
// Right now the init segment is saved in |output| for multi-segment
|
||||
// content. However, for TS all segments must be self-initializing so
|
||||
// there cannot be an init segment.
|
||||
if (stream.output.length()) {
|
||||
return Status(error::INVALID_ARGUMENT,
|
||||
"All TS segments must be self-initializing. Stream "
|
||||
"descriptors 'output' or 'init_segment' are not allowed.");
|
||||
}
|
||||
} else if (output_format == CONTAINER_WEBVTT ||
|
||||
output_format == CONTAINER_TTML ||
|
||||
output_format == CONTAINER_AAC || output_format == CONTAINER_MP3 ||
|
||||
output_format == CONTAINER_AC3 ||
|
||||
output_format == CONTAINER_EAC3) {
|
||||
if (output_format == CONTAINER_WEBVTT || output_format == CONTAINER_TTML ||
|
||||
output_format == CONTAINER_AAC || output_format == CONTAINER_MP3 ||
|
||||
output_format == CONTAINER_AC3 || output_format == CONTAINER_EAC3 ||
|
||||
output_format == CONTAINER_MPEG2TS) {
|
||||
// There is no need for an init segment when outputting because there is no
|
||||
// initialization data.
|
||||
if (stream.segment_template.length() && stream.output.length()) {
|
||||
return Status(
|
||||
error::INVALID_ARGUMENT,
|
||||
"Segmented subtitles or PackedAudio output cannot have an init "
|
||||
"Segmented subtitles, PackedAudio or TS output cannot have an init "
|
||||
"segment. Do not specify stream descriptors 'output' or "
|
||||
"'init_segment' when using 'segment_template'.");
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue