Shaka Packager SDK
Loading...
Searching...
No Matches
single_segment_segmenter.cc
1// Copyright 2014 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/mp4/single_segment_segmenter.h>
8
9#include <algorithm>
10#include <cstddef>
11#include <cstdint>
12#include <memory>
13#include <utility>
14#include <vector>
15
16#include <absl/log/check.h>
17#include <absl/log/log.h>
18
19#include <packager/file/file_closer.h>
20#include <packager/file/file_util.h>
21#include <packager/media/base/aes_cryptor.h>
22#include <packager/media/base/aes_encryptor.h>
23#include <packager/media/base/buffer_writer.h>
24#include <packager/media/base/fourccs.h>
25#include <packager/media/base/muxer_options.h>
26#include <packager/media/base/range.h>
27#include <packager/media/formats/mp4/box_definitions.h>
28#include <packager/media/formats/mp4/key_frame_info.h>
29#include <packager/media/formats/mp4/segmenter.h>
30#include <packager/status.h>
31
32namespace shaka {
33namespace media {
34namespace mp4 {
35
36SingleSegmentSegmenter::SingleSegmentSegmenter(const MuxerOptions& options,
37 std::unique_ptr<FileType> ftyp,
38 std::unique_ptr<Movie> moov)
39 : Segmenter(options, std::move(ftyp), std::move(moov)) {}
40
41SingleSegmentSegmenter::~SingleSegmentSegmenter() {
42 if (temp_file_)
43 temp_file_.release()->Close();
44 if (!temp_file_name_.empty()) {
45 if (!File::Delete(temp_file_name_.c_str()))
46 LOG(ERROR) << "Unable to delete temporary file " << temp_file_name_;
47 }
48}
49
50bool SingleSegmentSegmenter::GetInitRange(size_t* offset, size_t* size) {
51 // In Finalize, ftyp and moov gets written first so offset must be 0.
52 *offset = 0;
53 *size = ftyp()->ComputeSize() + moov()->ComputeSize();
54 return true;
55}
56
57bool SingleSegmentSegmenter::GetIndexRange(size_t* offset, size_t* size) {
58 // Index range is right after init range so the offset must be the size of
59 // ftyp and moov.
60 *offset = ftyp()->ComputeSize() + moov()->ComputeSize();
61 *size = options().mp4_params.generate_sidx_in_media_segments
62 ? vod_sidx_->ComputeSize()
63 : 0;
64 return true;
65}
66
67std::vector<Range> SingleSegmentSegmenter::GetSegmentRanges() {
68 std::vector<Range> ranges;
69 uint64_t next_offset = ftyp()->ComputeSize() + moov()->ComputeSize() +
70 (options().mp4_params.generate_sidx_in_media_segments
71 ? vod_sidx_->ComputeSize()
72 : 0) +
73 vod_sidx_->first_offset;
74 for (const SegmentReference& segment_reference : vod_sidx_->references) {
75 Range r;
76 r.start = next_offset;
77 // Ranges are inclusive, so -1 to the size.
78 r.end = r.start + segment_reference.referenced_size - 1;
79 next_offset = r.end + 1;
80 ranges.push_back(r);
81 }
82 return ranges;
83}
84
85Status SingleSegmentSegmenter::DoInitialize() {
86 // Single segment segmentation involves two stages:
87 // Stage 1: Create media subsegments from media samples
88 // Stage 2: Update media header (moov) which involves copying of media
89 // subsegments
90 // Assumes stage 2 takes similar amount of time as stage 1. The previous
91 // progress_target was set for stage 1. Times two to account for stage 2.
92 set_progress_target(progress_target() * 2);
93
94 if (!TempFilePath(options().temp_dir, &temp_file_name_))
95 return Status(error::FILE_FAILURE, "Unable to create temporary file.");
96 temp_file_.reset(File::Open(temp_file_name_.c_str(), "w"));
97 return temp_file_ ? Status::OK
98 : Status(error::FILE_FAILURE,
99 "Cannot open file to write " + temp_file_name_);
100}
101
102Status SingleSegmentSegmenter::DoFinalize() {
103 DCHECK(temp_file_);
104 DCHECK(ftyp());
105 DCHECK(moov());
106 DCHECK(vod_sidx_);
107
108 // Close the temp file to prepare for reading later.
109 if (!temp_file_.release()->Close()) {
110 return Status(
111 error::FILE_FAILURE,
112 "Cannot close the temp file " + temp_file_name_ +
113 ", possibly file permission issue or running out of disk space.");
114 }
115
116 std::unique_ptr<File, FileCloser> file(
117 File::Open(options().output_file_name.c_str(), "w"));
118 if (file == NULL) {
119 return Status(error::FILE_FAILURE,
120 "Cannot open file to write " + options().output_file_name);
121 }
122
123 LOG(INFO) << "Update media header (moov) and rewrite the file to '"
124 << options().output_file_name << "'.";
125
126 // Write ftyp, moov and sidx to output file.
127 std::unique_ptr<BufferWriter> buffer(new BufferWriter());
128 ftyp()->Write(buffer.get());
129 moov()->Write(buffer.get());
130
131 if (options().mp4_params.generate_sidx_in_media_segments)
132 vod_sidx_->Write(buffer.get());
133
134 Status status = buffer->WriteToFile(file.get());
135 if (!status.ok())
136 return status;
137
138 // Load the temp file and write to output file.
139 std::unique_ptr<File, FileCloser> temp_file(
140 File::Open(temp_file_name_.c_str(), "r"));
141 if (temp_file == NULL) {
142 return Status(error::FILE_FAILURE,
143 "Cannot open file to read " + temp_file_name_);
144 }
145
146 // The target of 2nd stage of single segment segmentation.
147 const uint64_t re_segment_progress_target = progress_target() * 0.5;
148
149 const int kBufSize = 0x200000; // 2MB.
150 std::unique_ptr<uint8_t[]> buf(new uint8_t[kBufSize]);
151 while (true) {
152 int64_t size = temp_file->Read(buf.get(), kBufSize);
153 if (size == 0) {
154 break;
155 } else if (size < 0) {
156 return Status(error::FILE_FAILURE,
157 "Failed to read file " + temp_file_name_);
158 }
159 int64_t size_written = file->Write(buf.get(), size);
160 if (size_written != size) {
161 return Status(error::FILE_FAILURE,
162 "Failed to write file " + options().output_file_name);
163 }
164 UpdateProgress(static_cast<double>(size) / temp_file->Size() *
165 re_segment_progress_target);
166 }
167 if (!temp_file.release()->Close()) {
168 return Status(error::FILE_FAILURE, "Cannot close the temp file " +
169 temp_file_name_ + " after reading.");
170 }
171 if (!file.release()->Close()) {
172 return Status(
173 error::FILE_FAILURE,
174 "Cannot close file " + options().output_file_name +
175 ", possibly file permission issue or running out of disk space.");
176 }
177 SetComplete();
178 return Status::OK;
179}
180
181Status SingleSegmentSegmenter::DoFinalizeSegment(int64_t segment_number) {
182 DCHECK(sidx());
183 DCHECK(fragment_buffer());
184 // sidx() contains pre-generated segment references with one reference per
185 // fragment. In VOD, this segment is converted into a subsegment, i.e. one
186 // reference, which contains all the fragments in sidx().
187 std::vector<SegmentReference>& refs = sidx()->references;
188 SegmentReference& vod_ref = refs[0];
189 int64_t first_sap_time =
190 refs[0].sap_delta_time + refs[0].earliest_presentation_time;
191 for (uint32_t i = 1; i < refs.size(); ++i) {
192 vod_ref.referenced_size += refs[i].referenced_size;
193 // NOTE: We calculate subsegment duration based on the total duration of
194 // this subsegment instead of subtracting earliest_presentation_time as
195 // indicated in the spec.
196 vod_ref.subsegment_duration += refs[i].subsegment_duration;
197 vod_ref.earliest_presentation_time = std::min(
198 vod_ref.earliest_presentation_time, refs[i].earliest_presentation_time);
199
200 if (vod_ref.sap_type == SegmentReference::TypeUnknown &&
201 refs[i].sap_type != SegmentReference::TypeUnknown) {
202 vod_ref.sap_type = refs[i].sap_type;
203 first_sap_time =
204 refs[i].sap_delta_time + refs[i].earliest_presentation_time;
205 }
206 }
207 // Calculate sap delta time w.r.t. earliest_presentation_time.
208 if (vod_ref.sap_type != SegmentReference::TypeUnknown) {
209 vod_ref.sap_delta_time =
210 first_sap_time - vod_ref.earliest_presentation_time;
211 }
212
213 Status status;
214 size_t segment_size = fragment_buffer()->Size();
215 if (aes128_encryption_config().protection_scheme == kAes128ProtectionScheme) {
216 // Encrypt this subsegment (all fragments accumulated since the last
217 // flush) as one standalone CBC stream, mirroring
218 // MultiSegmentSegmenter::WriteSegment's whole-segment encryption. Per
219 // RFC 8216 ยง5.2, PKCS7 padding is required. Doing this per-subsegment
220 // (rather than encrypting the whole single-file asset as one stream)
221 // keeps every HLS #EXT-X-BYTERANGE slice independently decryptable, the
222 // same access granularity multi-segment mode gets for free from having
223 // one file per segment. Without this, single_segment=true silently wrote
224 // the AES-128 config to the wrong segmenter and produced a CLEARTEXT
225 // asset while the manifest still advertised #EXT-X-KEY:METHOD=AES-128,
226 // so an unpadded plaintext byte range could never AES-CBC-decrypt. See
227 // https://github.com/shaka-project/shaka-packager/issues/1587.
228 std::vector<uint8_t> plaintext(
229 fragment_buffer()->Buffer(),
230 fragment_buffer()->Buffer() + fragment_buffer()->Size());
231 fragment_buffer()->Clear();
232
233 AesCbcEncryptor encryptor(kPkcs5Padding, AesCryptor::kUseConstantIv);
234 if (!encryptor.InitializeWithIv(aes128_encryption_config().key,
235 aes128_encryption_config().constant_iv)) {
236 return Status(error::ENCRYPTION_FAILURE,
237 "AES-128: failed to initialize encryptor for MP4 segment.");
238 }
239 std::vector<uint8_t> ciphertext;
240 if (!encryptor.Crypt(plaintext, &ciphertext)) {
241 return Status(error::ENCRYPTION_FAILURE,
242 "AES-128: segment encryption failed.");
243 }
244
245 // The manifest byte range must reflect what is actually written to
246 // disk, i.e. the padded ciphertext, not the natural (pre-encryption)
247 // fragment size accumulated above.
248 vod_ref.referenced_size = static_cast<uint32_t>(ciphertext.size());
249 segment_size = ciphertext.size();
250
251 BufferWriter ciphertext_buffer;
252 ciphertext_buffer.AppendVector(ciphertext);
253 status = ciphertext_buffer.WriteToFile(temp_file_.get());
254 // Key-frame byte offsets are meaningless once whole-subsegment CBC
255 // encryption is applied (a keyframe partway through a subsegment is no
256 // longer independently seekable in the ciphertext), so, like
257 // MultiSegmentSegmenter, skip I-frame playlist reporting for AES-128.
258 } else {
259 if (muxer_listener()) {
260 for (const KeyFrameInfo& key_frame_info : key_frame_infos()) {
261 // Unlike multisegment-segmenter, there is no (sub)segment header
262 // (styp, sidx), so this is already the offset within the
263 // (sub)segment.
264 muxer_listener()->OnKeyFrame(key_frame_info.timestamp,
265 key_frame_info.start_byte_offset,
266 key_frame_info.size);
267 }
268 }
269 // Append fragment buffer to temp file.
270 status = fragment_buffer()->WriteToFile(temp_file_.get());
271 }
272 if (!status.ok())
273 return status;
274
275 // Create segment if it does not exist yet.
276 if (vod_sidx_ == NULL) {
277 vod_sidx_.reset(new SegmentIndex());
278 vod_sidx_->reference_id = sidx()->reference_id;
279 vod_sidx_->timescale = sidx()->timescale;
280 vod_sidx_->earliest_presentation_time = vod_ref.earliest_presentation_time;
281 }
282 // Pushed after the AES-128 branch above (which may have corrected
283 // |vod_ref.referenced_size| to the actual ciphertext length) so
284 // GetSegmentRanges() reports byte ranges that match what was written.
285 vod_sidx_->references.push_back(vod_ref);
286
287 UpdateProgress(vod_ref.subsegment_duration);
288 if (muxer_listener()) {
289 muxer_listener()->OnSampleDurationReady(sample_duration());
290 muxer_listener()->OnNewSegment(
291 options().output_file_name, vod_ref.earliest_presentation_time,
292 vod_ref.subsegment_duration, segment_size, segment_number);
293 }
294 return Status::OK;
295}
296
297} // namespace mp4
298} // namespace media
299} // namespace shaka
All the methods that are virtual are virtual for mocking.
bool TempFilePath(const std::string &temp_dir, std::string *temp_file_path)
Definition file_util.cc:48