Shaka Packager SDK
Loading...
Searching...
No Matches
segmenter.cc
1// Copyright 2015 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/webm/segmenter.h>
8
9#include <cstdint>
10#include <cstring>
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#include <common/webmids.h>
19#include <mkvmuxer/mkvmuxer.h>
20#include <mkvmuxer/mkvmuxerutil.h>
21
22#include <packager/macros/logging.h>
23#include <packager/media/base/audio_stream_info.h>
24#include <packager/media/base/media_sample.h>
25#include <packager/media/base/muxer_options.h>
26#include <packager/media/base/stream_info.h>
27#include <packager/media/base/video_stream_info.h>
28#include <packager/media/codecs/vp_codec_configuration_record.h>
29#include <packager/media/event/muxer_listener.h>
30#include <packager/media/event/progress_listener.h>
31#include <packager/media/formats/webm/encryptor.h>
32#include <packager/media/formats/webm/mkv_writer.h>
33#include <packager/media/formats/webm/webm_constants.h>
34#include <packager/status.h>
35#include <packager/version/version.h>
36
37using mkvmuxer::AudioTrack;
38using mkvmuxer::VideoTrack;
39
40namespace shaka {
41namespace media {
42namespace webm {
43namespace {
44const int64_t kTimecodeScale = 1000000;
45const int64_t kSecondsToNs = 1000000000L;
46
47// Round to closest integer.
48uint64_t Round(double value) {
49 return static_cast<uint64_t>(value + 0.5);
50}
51
52// There are three different kinds of timestamp here:
53// (1) ISO-BMFF timestamp (seconds scaled by ISO-BMFF timescale)
54// This is used in our MediaSample and StreamInfo structures.
55// (2) WebM timecode (seconds scaled by kSecondsToNs / WebM timecode scale)
56// This is used in most WebM structures.
57// (3) Nanoseconds (seconds scaled by kSecondsToNs)
58// This is used in some WebM structures, e.g. Frame.
59// We use Nanoseconds as intermediate format here for conversion, in
60// uint64_t/int64_t, which is sufficient to represent a time as large as 292
61// years.
62
63int64_t BmffTimestampToNs(int64_t timestamp, int64_t time_scale) {
64 // Casting to double is needed otherwise kSecondsToNs * timestamp may overflow
65 // uint64_t/int64_t.
66 return Round(static_cast<double>(timestamp) / time_scale * kSecondsToNs);
67}
68
69int64_t NsToBmffTimestamp(int64_t ns, int64_t time_scale) {
70 // Casting to double is needed otherwise ns * time_scale may overflow
71 // uint64_t/int64_t.
72 return Round(static_cast<double>(ns) / kSecondsToNs * time_scale);
73}
74
75int64_t NsToWebMTimecode(int64_t ns, int64_t timecode_scale) {
76 return ns / timecode_scale;
77}
78
79int64_t WebMTimecodeToNs(int64_t timecode, int64_t timecode_scale) {
80 return timecode * timecode_scale;
81}
82
83} // namespace
84
85Segmenter::Segmenter(const MuxerOptions& options) : options_(options) {}
86
87Segmenter::~Segmenter() {}
88
89Status Segmenter::Initialize(const StreamInfo& info,
90 ProgressListener* progress_listener,
91 MuxerListener* muxer_listener) {
92 is_encrypted_ = info.is_encrypted();
93 duration_ = info.duration();
94 time_scale_ = info.time_scale();
95
96 muxer_listener_ = muxer_listener;
97
98 // Use media duration as progress target.
99 progress_target_ = info.duration();
100 progress_listener_ = progress_listener;
101
102 segment_info_.Init();
103 segment_info_.set_timecode_scale(kTimecodeScale);
104
105 const std::string version = GetPackagerVersion();
106 if (!version.empty()) {
107 segment_info_.set_writing_app(
108 (GetPackagerProjectUrl() + " version " + version).c_str());
109 }
110
111 if (options().segment_template.empty()) {
112 // Set an initial duration so the duration element is written; will be
113 // overwritten at the end. This works because this is a float and floats
114 // are always the same size.
115 segment_info_.set_duration(1);
116 }
117
118 // Create the track info.
119 // The seed is only used to create a UID which we overwrite later.
120 unsigned int seed = 0;
121 std::unique_ptr<mkvmuxer::Track> track;
122 Status status;
123 switch (info.stream_type()) {
124 case kStreamVideo: {
125 std::unique_ptr<VideoTrack> video_track(new VideoTrack(&seed));
126 status = InitializeVideoTrack(static_cast<const VideoStreamInfo&>(info),
127 video_track.get());
128 track = std::move(video_track);
129 break;
130 }
131 case kStreamAudio: {
132 std::unique_ptr<AudioTrack> audio_track(new AudioTrack(&seed));
133 status = InitializeAudioTrack(static_cast<const AudioStreamInfo&>(info),
134 audio_track.get());
135 track = std::move(audio_track);
136 break;
137 }
138 default:
139 NOTIMPLEMENTED() << "Not implemented for stream type: "
140 << info.stream_type();
141 status = Status(error::UNIMPLEMENTED, "Not implemented for stream type");
142 }
143 if (!status.ok())
144 return status;
145
146 if (info.is_encrypted()) {
147 if (info.encryption_config().per_sample_iv_size != kWebMIvSize)
148 return Status(error::MUXER_FAILURE, "Incorrect size WebM encryption IV.");
149 status =
150 UpdateTrackForEncryption(info.encryption_config().key_id, track.get());
151 if (!status.ok())
152 return status;
153 }
154
155 tracks_.AddTrack(track.get(), info.track_id());
156 // number() is only available after the above instruction.
157 track_id_ = track->number();
158 // |tracks_| owns |track|.
159 track.release();
160 return DoInitialize();
161}
162
163Status Segmenter::Finalize() {
164 if (prev_sample_ && !prev_sample_->end_of_stream()) {
165 int64_t duration =
166 prev_sample_->pts() - first_timestamp_ + prev_sample_->duration();
167 segment_info_.set_duration(FromBmffTimestamp(duration));
168 }
169 return DoFinalize();
170}
171
172Status Segmenter::AddSample(const MediaSample& source_sample) {
173 std::shared_ptr<MediaSample> sample(source_sample.Clone());
174
175 // The duration of the first sample may have been adjusted, so use
176 // the duration of the second sample instead.
177 if (num_samples_ < 2) {
178 sample_durations_[num_samples_] = sample->duration();
179 if (num_samples_ == 0)
180 first_timestamp_ = sample->pts();
181 else if (muxer_listener_)
182 muxer_listener_->OnSampleDurationReady(sample_durations_[num_samples_]);
183 num_samples_++;
184 }
185
186 UpdateProgress(sample->duration());
187
188 // This writes frames in a delay. Meaning that the previous frame is written
189 // on this call to AddSample. The current frame is stored until the next
190 // call. This is done to determine which frame is the last in a Cluster.
191 // This first block determines if this is a new Cluster and writes the
192 // previous frame first before creating the new Cluster.
193
194 Status status;
195 if (new_segment_ || new_subsegment_) {
196 status = NewSegment(sample->pts(), new_subsegment_);
197 } else {
198 status = WriteFrame(false /* write_duration */);
199 }
200 if (!status.ok())
201 return status;
202
203 if (is_encrypted_)
204 UpdateFrameForEncryption(sample.get());
205
206 new_subsegment_ = false;
207 new_segment_ = false;
208 prev_sample_ = sample;
209 return Status::OK;
210}
211
212Status Segmenter::FinalizeSegment(int64_t /*start_timestamp*/,
213 int64_t /*duration_timestamp*/,
214 bool is_subsegment,
215 int64_t segment_number) {
216 if (is_subsegment)
217 new_subsegment_ = true;
218 else
219 new_segment_ = true;
220 return WriteFrame(true /* write duration */);
221}
222
223float Segmenter::GetDurationInSeconds() const {
224 return WebMTimecodeToNs(segment_info_.duration(),
225 segment_info_.timecode_scale()) /
226 static_cast<double>(kSecondsToNs);
227}
228
229int64_t Segmenter::FromBmffTimestamp(int64_t bmff_timestamp) {
230 return NsToWebMTimecode(BmffTimestampToNs(bmff_timestamp, time_scale_),
231 segment_info_.timecode_scale());
232}
233
234int64_t Segmenter::FromWebMTimecode(int64_t webm_timecode) {
235 return NsToBmffTimestamp(
236 WebMTimecodeToNs(webm_timecode, segment_info_.timecode_scale()),
237 time_scale_);
238}
239
240Status Segmenter::WriteSegmentHeader(uint64_t file_size, MkvWriter* writer) {
241 Status error_status(error::FILE_FAILURE, "Error writing segment header.");
242
243 if (!WriteEbmlHeader(writer))
244 return error_status;
245
246 if (WriteID(writer, libwebm::kMkvSegment) != 0)
247 return error_status;
248
249 const uint64_t segment_size_size = 8;
250 segment_payload_pos_ = writer->Position() + segment_size_size;
251 if (file_size > 0) {
252 // We want the size of the segment element, so subtract the header.
253 if (WriteUIntSize(writer, file_size - segment_payload_pos_,
254 segment_size_size) != 0)
255 return error_status;
256 if (!seek_head_.Write(writer))
257 return error_status;
258 } else {
259 if (SerializeInt(writer, mkvmuxer::kEbmlUnknownValue, segment_size_size) !=
260 0)
261 return error_status;
262 // We don't know the header size, so write a placeholder.
263 if (!seek_head_.WriteVoid(writer))
264 return error_status;
265 }
266
267 seek_head_.set_info_pos(writer->Position() - segment_payload_pos_);
268 if (!segment_info_.Write(writer))
269 return error_status;
270
271 seek_head_.set_tracks_pos(writer->Position() - segment_payload_pos_);
272 if (!tracks_.Write(writer))
273 return error_status;
274
275 return Status::OK;
276}
277
278Status Segmenter::SetCluster(int64_t start_webm_timecode,
279 uint64_t position,
280 MkvWriter* writer) {
281 const int64_t scale = segment_info_.timecode_scale();
282 cluster_.reset(new mkvmuxer::Cluster(start_webm_timecode, position, scale));
283 cluster_->Init(writer);
284 return Status::OK;
285}
286
287void Segmenter::UpdateProgress(uint64_t progress) {
288 accumulated_progress_ += progress;
289 if (!progress_listener_ || progress_target_ == 0)
290 return;
291 // It might happen that accumulated progress exceeds progress_target due to
292 // computation errors, e.g. rounding error. Cap it so it never reports > 100%
293 // progress.
294 if (accumulated_progress_ >= progress_target_) {
295 progress_listener_->OnProgress(1.0);
296 } else {
297 progress_listener_->OnProgress(static_cast<double>(accumulated_progress_) /
298 progress_target_);
299 }
300}
301
302Status Segmenter::InitializeVideoTrack(const VideoStreamInfo& info,
303 VideoTrack* track) {
304 if (info.codec() == kCodecAV1) {
305 track->set_codec_id("V_AV1");
306 if (!track->SetCodecPrivate(info.codec_config().data(),
307 info.codec_config().size())) {
308 return Status(error::INTERNAL_ERROR,
309 "Private codec data required for AV1 streams");
310 }
311 } else if (info.codec() == kCodecVP8) {
312 track->set_codec_id("V_VP8");
313 } else if (info.codec() == kCodecVP9) {
314 track->set_codec_id("V_VP9");
315
316 // The |StreamInfo::codec_config| field is stored using the MP4 format; we
317 // need to convert it to the WebM format.
318 VPCodecConfigurationRecord vp_config;
319 if (!vp_config.ParseMP4(info.codec_config())) {
320 return Status(error::INTERNAL_ERROR,
321 "Unable to parse VP9 codec configuration");
322 }
323
324 mkvmuxer::Colour colour;
325 if (vp_config.matrix_coefficients() != AVCOL_SPC_UNSPECIFIED) {
326 colour.set_matrix_coefficients(vp_config.matrix_coefficients());
327 }
328 if (vp_config.transfer_characteristics() != AVCOL_TRC_UNSPECIFIED) {
329 colour.set_transfer_characteristics(vp_config.transfer_characteristics());
330 }
331 if (vp_config.color_primaries() != AVCOL_PRI_UNSPECIFIED) {
332 colour.set_primaries(vp_config.color_primaries());
333 }
334 if (!track->SetColour(colour)) {
335 return Status(error::INTERNAL_ERROR,
336 "Failed to setup color element for VPx streams");
337 }
338
339 std::vector<uint8_t> codec_config;
340 vp_config.WriteWebM(&codec_config);
341 if (!track->SetCodecPrivate(codec_config.data(), codec_config.size())) {
342 return Status(error::INTERNAL_ERROR,
343 "Private codec data required for VPx streams");
344 }
345 } else {
346 LOG(ERROR) << "Only VP8, VP9 and AV1 video codecs are supported in WebM.";
347 return Status(error::UNIMPLEMENTED,
348 "Only VP8, VP9 and AV1 video codecs are supported in WebM.");
349 }
350
351 track->set_uid(info.track_id());
352 if (!info.language().empty())
353 track->set_language(info.language().c_str());
354 track->set_type(mkvmuxer::Tracks::kVideo);
355 track->set_width(info.width());
356 track->set_height(info.height());
357 track->set_display_height(info.height());
358 track->set_display_width(info.width() * info.pixel_width() /
359 info.pixel_height());
360 return Status::OK;
361}
362
363Status Segmenter::InitializeAudioTrack(const AudioStreamInfo& info,
364 AudioTrack* track) {
365 if (info.codec() == kCodecOpus) {
366 track->set_codec_id(mkvmuxer::Tracks::kOpusCodecId);
367 } else if (info.codec() == kCodecVorbis) {
368 track->set_codec_id(mkvmuxer::Tracks::kVorbisCodecId);
369 } else {
370 LOG(ERROR) << "Only Vorbis and Opus audio codec are supported in WebM.";
371 return Status(error::UNIMPLEMENTED,
372 "Only Vorbis and Opus audio codecs are supported in WebM.");
373 }
374 if (!track->SetCodecPrivate(info.codec_config().data(),
375 info.codec_config().size())) {
376 return Status(error::INTERNAL_ERROR,
377 "Private codec data required for audio streams");
378 }
379
380 track->set_uid(info.track_id());
381 if (!info.language().empty())
382 track->set_language(info.language().c_str());
383 track->set_type(mkvmuxer::Tracks::kAudio);
384 track->set_sample_rate(info.sampling_frequency());
385 track->set_channels(info.num_channels());
386 track->set_seek_pre_roll(info.seek_preroll_ns());
387 track->set_codec_delay(info.codec_delay_ns());
388 return Status::OK;
389}
390
391Status Segmenter::WriteFrame(bool write_duration) {
392 // Create a frame manually so we can create non-SimpleBlock frames. This
393 // is required to allow the frame duration to be added. If the duration
394 // is not set, then a SimpleBlock will still be written.
395 mkvmuxer::Frame frame;
396
397 if (!frame.Init(prev_sample_->data(), prev_sample_->data_size())) {
398 return Status(error::MUXER_FAILURE,
399 "Error adding sample to segment: Frame::Init failed");
400 }
401
402 if (write_duration) {
403 frame.set_duration(
404 BmffTimestampToNs(prev_sample_->duration(), time_scale_));
405 }
406 frame.set_is_key(prev_sample_->is_key_frame());
407 frame.set_timestamp(BmffTimestampToNs(prev_sample_->pts(), time_scale_));
408 frame.set_track_number(track_id_);
409
410 if (prev_sample_->side_data_size() > 0) {
411 uint64_t block_add_id;
412 // First 8 bytes of side_data is the BlockAddID element's value, which is
413 // done to mimic ffmpeg behavior. See webm_cluster_parser.cc for details.
414 CHECK_GT(prev_sample_->side_data_size(), sizeof(block_add_id));
415 memcpy(&block_add_id, prev_sample_->side_data(), sizeof(block_add_id));
416 if (!frame.AddAdditionalData(
417 prev_sample_->side_data() + sizeof(block_add_id),
418 prev_sample_->side_data_size() - sizeof(block_add_id),
419 block_add_id)) {
420 return Status(
421 error::MUXER_FAILURE,
422 "Error adding sample to segment: Frame::AddAditionalData Failed");
423 }
424 }
425
426 if (!prev_sample_->is_key_frame() && !frame.CanBeSimpleBlock()) {
427 frame.set_reference_block_timestamp(
428 BmffTimestampToNs(reference_frame_timestamp_, time_scale_));
429 }
430
431 // GetRelativeTimecode will return -1 if the relative timecode is too large
432 // to fit in the frame.
433 if (cluster_->GetRelativeTimecode(NsToWebMTimecode(
434 frame.timestamp(), cluster_->timecode_scale())) < 0) {
435 const double segment_duration =
436 static_cast<double>(frame.timestamp() -
437 WebMTimecodeToNs(cluster_->timecode(),
438 cluster_->timecode_scale())) /
439 kSecondsToNs;
440 LOG(ERROR) << "Error adding sample to segment: segment too large, "
441 << segment_duration
442 << " seconds. Please check your GOP size and segment duration.";
443 return Status(error::MUXER_FAILURE,
444 "Error adding sample to segment: segment too large");
445 }
446
447 if (!cluster_->AddFrame(&frame)) {
448 return Status(error::MUXER_FAILURE,
449 "Error adding sample to segment: Cluster::AddFrame failed");
450 }
451
452 // A reference frame is needed for non-keyframes. Having a reference to the
453 // previous block is good enough.
454 // See libwebm Segment::AddGenericFrame
455 reference_frame_timestamp_ = prev_sample_->pts();
456 return Status::OK;
457}
458
459} // namespace webm
460} // namespace media
461} // namespace shaka
Holds audio stream information.
Class to hold a media sample.
std::shared_ptr< MediaSample > Clone() const
Clone the object and return a new MediaSample.
An implementation of IMkvWriter using our File type.
Definition mkv_writer.h:26
mkvmuxer::int64 Position() const override
Definition mkv_writer.cc:83
This class listens to progress updates events.
Abstract class holds stream information.
Definition stream_info.h:73
Holds video stream information.
All the methods that are virtual are virtual for mocking.