Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for single file TS for HLS #934

Merged
merged 3 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions packager/app/test/packager_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1590,6 +1590,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