Shaka Packager SDK
Loading...
Searching...
No Matches
ts_muxer.cc
1// Copyright 2016 Google LLC. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file or at
5// https://developers.google.com/open-source/licenses/bsd
6
7#include <packager/media/formats/mp2t/ts_muxer.h>
8
9#include <cstddef>
10#include <cstdint>
11#include <memory>
12#include <string>
13#include <utility>
14#include <vector>
15
16#include <absl/log/check.h>
17#include <absl/log/log.h>
18
19#include <packager/file.h>
20#include <packager/file/file_closer.h>
21#include <packager/macros/status.h>
22#include <packager/media/base/aes_cryptor.h>
23#include <packager/media/base/aes_encryptor.h>
24#include <packager/media/base/buffer_writer.h>
25#include <packager/media/base/fourccs.h>
26#include <packager/media/base/media_handler.h>
27#include <packager/media/base/media_sample.h>
28#include <packager/media/base/muxer.h>
29#include <packager/media/base/muxer_options.h>
30#include <packager/media/base/muxer_util.h>
31#include <packager/media/base/range.h>
32#include <packager/media/event/muxer_listener.h>
33#include <packager/media/formats/mp2t/ts_segmenter.h>
34#include <packager/status.h>
35
36namespace shaka {
37namespace media {
38namespace mp2t {
39
40namespace {
41const int32_t kTsTimescale = 90000;
42} // namespace
43
44TsMuxer::TsMuxer(const MuxerOptions& muxer_options) : Muxer(muxer_options) {}
45TsMuxer::~TsMuxer() {}
46
47Status TsMuxer::InitializeMuxer() {
48 if (streams().size() > 1u)
49 return Status(error::MUXER_FAILURE, "Cannot handle more than one streams.");
50
51 if (options().segment_template.empty()) {
52 const std::string& file_name = options().output_file_name;
53 DCHECK(!file_name.empty());
54 output_file_.reset(File::Open(file_name.c_str(), "w"));
55 if (!output_file_) {
56 return Status(error::FILE_FAILURE,
57 "Cannot open file for write " + file_name);
58 }
59 }
60
61 segmenter_.reset(new TsSegmenter(options(), muxer_listener()));
62 Status status = segmenter_->Initialize(*streams()[0]);
63
64 // Capture AES-128 encryption config once so FinalizeSegment can encrypt.
65 const auto& stream = *streams()[0];
66 if (stream.is_encrypted() &&
67 stream.encryption_config().protection_scheme == kAes128ProtectionScheme) {
68 aes128_encryption_config_ = stream.encryption_config();
69 }
70
71 FireOnMediaStartEvent();
72 return status;
73}
74
75Status TsMuxer::Finalize() {
76 FireOnMediaEndEvent();
77 return segmenter_->Finalize();
78}
79
80Status TsMuxer::AddMediaSample(size_t stream_id, const MediaSample& sample) {
81 DCHECK_EQ(stream_id, 0u);
82
83 // The duration of the first sample may have been adjusted, so use
84 // the duration of the second sample instead.
85 if (num_samples_ < 2) {
86 sample_durations_[num_samples_] =
87 sample.duration() * kTsTimescale / streams().front()->time_scale();
88 if (num_samples_ == 1 && muxer_listener())
89 muxer_listener()->OnSampleDurationReady(sample_durations_[num_samples_]);
90 num_samples_++;
91 }
92 return segmenter_->AddSample(sample);
93}
94
95Status TsMuxer::FinalizeSegment(size_t stream_id,
96 const SegmentInfo& segment_info) {
97 DCHECK_EQ(stream_id, 0u);
98
99 if (segment_info.is_subsegment)
100 return Status::OK;
101
102 Status s = segmenter_->FinalizeSegment(segment_info.start_timestamp,
103 segment_info.duration);
104 if (!s.ok())
105 return s;
106 if (!segmenter_->segment_started())
107 return Status::OK;
108
109 int64_t segment_start_timestamp = segmenter_->segment_start_timestamp();
110
111 std::string segment_path =
112 options().segment_template.empty()
113 ? options().output_file_name
114 : GetSegmentName(options().segment_template, segment_start_timestamp,
115 segment_info.segment_number, options().bandwidth);
116
117 const int64_t file_size = segmenter_->segment_buffer()->Size();
118
119 EncryptSegmentIfNeeded(segmenter_->segment_buffer());
120
121 RETURN_IF_ERROR(WriteSegment(segment_path, segmenter_->segment_buffer()));
122
123 total_duration_ += segment_info.duration;
124
125 if (muxer_listener()) {
126 muxer_listener()->OnNewSegment(
127 segment_path,
128 segment_info.start_timestamp * segmenter_->timescale() +
129 segmenter_->transport_stream_timestamp_offset(),
130 segment_info.duration * segmenter_->timescale(), file_size,
131 segment_info.segment_number);
132 }
133
134 segmenter_->set_segment_started(false);
135
136 return Status::OK;
137}
138
139void TsMuxer::EncryptSegmentIfNeeded(BufferWriter* segment_buffer) {
140 if (aes128_encryption_config_.protection_scheme != kAes128ProtectionScheme)
141 return;
142 if (segment_buffer->Size() == 0)
143 return;
144
145 // AES-128 HLS: encrypt the entire TS segment as one CBC stream.
146 // Per RFC 8216 ยง5.2, PKCS7 padding is required; TS packet size (188 bytes)
147 // is not a multiple of 16, so segments are not guaranteed to be
148 // block-aligned.
149 AesCbcEncryptor encryptor(kPkcs5Padding, AesCryptor::kUseConstantIv);
150 if (!encryptor.InitializeWithIv(aes128_encryption_config_.key,
151 aes128_encryption_config_.constant_iv)) {
152 LOG(ERROR) << "AES-128: failed to initialize encryptor for segment.";
153 return;
154 }
155
156 std::vector<uint8_t> plaintext(
157 segment_buffer->Buffer(),
158 segment_buffer->Buffer() + segment_buffer->Size());
159 std::vector<uint8_t> ciphertext;
160 if (!encryptor.Crypt(plaintext, &ciphertext)) {
161 LOG(ERROR) << "AES-128: encryption failed for segment.";
162 return;
163 }
164 segment_buffer->Clear();
165 segment_buffer->AppendVector(ciphertext);
166}
167
168Status TsMuxer::WriteSegment(const std::string& segment_path,
169 BufferWriter* segment_buffer) {
170 std::unique_ptr<File, FileCloser> file;
171
172 if (output_file_) {
173 // This is in single segment mode.
174 Range range;
175 range.start = media_ranges_.subsegment_ranges.empty()
176 ? 0
177 : (media_ranges_.subsegment_ranges.back().end + 1);
178 range.end = range.start + segment_buffer->Size() - 1;
179 media_ranges_.subsegment_ranges.push_back(range);
180 } else {
181 file.reset(File::Open(segment_path.c_str(), "w"));
182 if (!file) {
183 return Status(error::FILE_FAILURE,
184 "Cannot open file for write " + segment_path);
185 }
186 }
187
188 RETURN_IF_ERROR(segment_buffer->WriteToFile(output_file_ ? output_file_.get()
189 : file.get()));
190
191 if (file)
192 RETURN_IF_ERROR(CloseFile(std::move(file)));
193 return Status::OK;
194}
195
196Status TsMuxer::CloseFile(std::unique_ptr<File, FileCloser> file) {
197 std::string file_name = file->file_name();
198 if (!file.release()->Close()) {
199 return Status(
200 error::FILE_FAILURE,
201 "Cannot close file " + file_name +
202 ", possibly file permission issue or running out of disk space.");
203 }
204 return Status::OK;
205}
206
207void TsMuxer::FireOnMediaStartEvent() {
208 if (!muxer_listener())
209 return;
210 muxer_listener()->OnMediaStart(options(), *streams().front(), kTsTimescale,
211 MuxerListener::kContainerMpeg2ts);
212}
213
214void TsMuxer::FireOnMediaEndEvent() {
215 if (!muxer_listener())
216 return;
217
218 muxer_listener()->OnMediaEnd(media_ranges_, total_duration_);
219}
220
221} // namespace mp2t
222} // namespace media
223} // namespace shaka
All the methods that are virtual are virtual for mocking.