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:
sr90 2024-02-23 15:28:11 -08:00 committed by GitHub
parent 6acdcc394a
commit 4aa4b4b9aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 186 additions and 86 deletions

View File

@ -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(

View File

@ -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

View 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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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);
};

View File

@ -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;
}

View File

@ -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_;

View File

@ -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(

View File

@ -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'.");
}