Skip to content

Commit

Permalink
feat: Add support for single file TS for HLS (#934)
Browse files Browse the repository at this point in the history
This is based on comments at
#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>
  • Loading branch information
sr1990 and cosmin authored Feb 23, 2024
1 parent 6acdcc3 commit 4aa4b4b
Show file tree
Hide file tree
Showing 13 changed files with 186 additions and 86 deletions.
7 changes: 7 additions & 0 deletions packager/app/test/packager_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1597,6 +1597,13 @@ def testEc3AndHlsSingleSegmentMp4Encrypted(self):
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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 11 additions & 0 deletions packager/app/test/testdata/hls-single-segment-ts/output.m3u8
Original file line number Diff line number Diff line change
@@ -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"
97 changes: 89 additions & 8 deletions packager/media/formats/mp2t/ts_muxer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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();
Expand All @@ -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() {
Expand All @@ -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
Expand Down
15 changes: 15 additions & 0 deletions packager/media/formats/mp2t/ts_muxer.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,28 @@ 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();

std::unique_ptr<TsSegmenter> segmenter_;
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);
};

Expand Down
39 changes: 1 addition & 38 deletions packager/media/formats/mp2t/ts_segmenter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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_(
Expand All @@ -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.");
Expand Down Expand Up @@ -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;
}

Expand Down
20 changes: 12 additions & 8 deletions packager/media/formats/mp2t/ts_segmenter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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.
Expand All @@ -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_;

Expand Down
11 changes: 0 additions & 11 deletions packager/media/formats/mp2t/ts_segmenter_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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(
Expand Down
Loading

0 comments on commit 4aa4b4b

Please sign in to comment.