Shaka Packager SDK
Loading...
Searching...
No Matches
cue_alignment_handler.cc
1// Copyright 2018 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/chunking/cue_alignment_handler.h>
8
9#include <algorithm>
10#include <cstddef>
11#include <cstdint>
12#include <memory>
13#include <utility>
14
15#include <absl/log/check.h>
16#include <absl/log/log.h>
17
18#include <packager/macros/logging.h>
19#include <packager/macros/status.h>
20#include <packager/media/base/media_handler.h>
21#include <packager/media/base/stream_info.h>
22#include <packager/status.h>
23
24namespace shaka {
25namespace media {
26namespace {
27// The max number of samples that are allowed to be buffered before we shutdown
28// because there is likely a problem with the content or how the pipeline was
29// configured. This is about 20 seconds of buffer for audio with 48kHz.
30const size_t kMaxBufferSize = 1000;
31
32int64_t GetScaledTime(const StreamInfo& info, const StreamData& data) {
33 DCHECK(data.text_sample || data.media_sample);
34
35 if (data.text_sample) {
36 return data.text_sample->start_time();
37 }
38
39 if (info.stream_type() == kStreamText) {
40 // This class does not support splitting MediaSample at cue points, which is
41 // required for text stream. This class expects MediaSample to be converted
42 // to TextSample before passing to this class.
43 NOTIMPLEMENTED()
44 << "A text streams should use text samples, not media samples.";
45 }
46
47 if (info.stream_type() == kStreamAudio) {
48 // Return the mid-point for audio because if the portion of the sample
49 // after the cue point is bigger than the portion of the sample before
50 // the cue point, the sample is placed after the cue.
51 return data.media_sample->pts() + data.media_sample->duration() / 2;
52 }
53
54 DCHECK_EQ(info.stream_type(), kStreamVideo);
55 return data.media_sample->pts();
56}
57
58double TimeInSeconds(const StreamInfo& info, const StreamData& data) {
59 const int64_t scaled_time = GetScaledTime(info, data);
60 const int32_t time_scale = info.time_scale();
61
62 return static_cast<double>(scaled_time) / time_scale;
63}
64
65double TextEndTimeInSeconds(const StreamInfo& info, const StreamData& data) {
66 DCHECK(data.text_sample);
67
68 const int64_t scaled_time = data.text_sample->EndTime();
69 const int32_t time_scale = info.time_scale();
70
71 return static_cast<double>(scaled_time) / time_scale;
72}
73
74Status GetNextCue(double hint,
75 SyncPointQueue* sync_points,
76 std::shared_ptr<const CueEvent>* out_cue) {
77 DCHECK(sync_points);
78 DCHECK(out_cue);
79
80 *out_cue = sync_points->GetNext(hint);
81
82 // |*out_cue| will only be null if the job was cancelled.
83 return *out_cue ? Status::OK
84 : Status(error::CANCELLED, "SyncPointQueue is cancelled.");
85}
86} // namespace
87
88CueAlignmentHandler::CueAlignmentHandler(SyncPointQueue* sync_points)
89 : sync_points_(sync_points) {}
90
91Status CueAlignmentHandler::InitializeInternal() {
92 sync_points_->AddThread();
93 stream_states_.resize(num_input_streams());
94
95 // Get the first hint for the stream. Use a negative hint so that if there is
96 // suppose to be a sync point at zero, we will still respect it.
97 hint_ = sync_points_->GetHint(-1);
98
99 return Status::OK;
100}
101
102Status CueAlignmentHandler::Process(std::unique_ptr<StreamData> data) {
103 switch (data->stream_data_type) {
104 case StreamDataType::kStreamInfo:
105 return OnStreamInfo(std::move(data));
106 case StreamDataType::kTextSample:
107 case StreamDataType::kMediaSample:
108 return OnSample(std::move(data));
109 default:
110 VLOG(3) << "Dropping unsupported data type "
111 << static_cast<int>(data->stream_data_type);
112 return Status::OK;
113 }
114}
115
116Status CueAlignmentHandler::OnFlushRequest(size_t stream_index) {
117 stream_states_[stream_index].to_be_flushed = true;
118
119 // We need to wait for all stream to flush before we can flush each stream.
120 // This allows cached buffers to be cleared and cues to be properly
121 // synchronized and set on all streams.
122 for (const StreamState& stream_state : stream_states_) {
123 if (!stream_state.to_be_flushed) {
124 return Status::OK;
125 }
126 }
127
128 // Do a once over all the streams to ensure that their states are as we expect
129 // them. Video and non-video streams have different allowances here. Video
130 // should absolutely have no cues or samples where as non-video streams may
131 // have cues or samples.
132 for (StreamState& stream : stream_states_) {
133 DCHECK(stream.to_be_flushed);
134
135 if (stream.info->stream_type() == kStreamVideo) {
136 DCHECK_EQ(stream.samples.size(), 0u)
137 << "Video streams should not store samples";
138 DCHECK_EQ(stream.cues.size(), 0u)
139 << "Video streams should not store cues";
140 }
141 }
142
143 // It is possible that we did not get all the cues. |hint_| will get updated
144 // when we call |UseNextSyncPoint|.
145 while (sync_points_->HasMore(hint_)) {
146 std::shared_ptr<const CueEvent> next_cue;
147 RETURN_IF_ERROR(GetNextCue(hint_, sync_points_, &next_cue));
148 RETURN_IF_ERROR(UseNewSyncPoint(std::move(next_cue)));
149 }
150
151 // Now that there are new cues, it may be possible to dispatch some of the
152 // samples that may be left waiting.
153 for (StreamState& stream : stream_states_) {
154 RETURN_IF_ERROR(RunThroughSamples(&stream));
155 DCHECK_EQ(stream.samples.size(), 0u);
156
157 // Ignore extra cues at the end, except for text, as they will result in
158 // empty DASH Representations, which is not spec compliant.
159 // For text, if the cue is before the max end time, it will still be
160 // dispatched as the text samples intercepted by the cue can be split into
161 // two at the cue point.
162 for (auto& cue : stream.cues) {
163 // |max_text_sample_end_time_seconds| is always 0 for non-text samples.
164 if (cue->cue_event->time_in_seconds <
165 stream.max_text_sample_end_time_seconds) {
166 RETURN_IF_ERROR(Dispatch(std::move(cue)));
167 } else {
168 VLOG(1) << "Ignore extra cue in stream " << cue->stream_index
169 << " with time " << cue->cue_event->time_in_seconds
170 << "s in the end.";
171 }
172 }
173 stream.cues.clear();
174 }
175
176 return FlushAllDownstreams();
177}
178
179Status CueAlignmentHandler::OnStreamInfo(std::unique_ptr<StreamData> data) {
180 StreamState& stream_state = stream_states_[data->stream_index];
181 // Keep a copy of the stream info so that we can check type and check
182 // timescale.
183 stream_state.info = data->stream_info;
184
185 return Dispatch(std::move(data));
186}
187
188Status CueAlignmentHandler::OnVideoSample(std::unique_ptr<StreamData> sample) {
189 DCHECK(sample);
190 DCHECK(sample->media_sample);
191
192 const size_t stream_index = sample->stream_index;
193 StreamState& stream = stream_states_[stream_index];
194
195 const double sample_time = TimeInSeconds(*stream.info, *sample);
196 const bool is_key_frame = sample->media_sample->is_key_frame();
197
198 if (is_key_frame && sample_time >= hint_) {
199 auto next_sync = sync_points_->PromoteAt(sample_time);
200
201 if (!next_sync) {
202 LOG(ERROR) << "Failed to promote sync point at " << sample_time
203 << ". This happens only if video streams are not GOP-aligned.";
204 return Status(error::INVALID_ARGUMENT,
205 "Streams are not properly GOP-aligned.");
206 }
207
208 RETURN_IF_ERROR(UseNewSyncPoint(std::move(next_sync)));
209 DCHECK_EQ(stream.cues.size(), 1u);
210 RETURN_IF_ERROR(Dispatch(std::move(stream.cues.front())));
211 stream.cues.pop_front();
212 }
213
214 return Dispatch(std::move(sample));
215}
216
217Status CueAlignmentHandler::OnNonVideoSample(
218 std::unique_ptr<StreamData> sample) {
219 DCHECK(sample);
220 DCHECK(sample->media_sample || sample->text_sample);
221
222 const size_t stream_index = sample->stream_index;
223 StreamState& stream_state = stream_states_[stream_index];
224
225 // Accept the sample. This will output it if it comes before the hint point or
226 // will cache it if it comes after the hint point.
227 RETURN_IF_ERROR(AcceptSample(std::move(sample), &stream_state));
228
229 // If all the streams are waiting on a hint, it means that none has next sync
230 // point determined. It also means that there are no video streams and we need
231 // to wait for all streams to converge on a hint so that we can get the next
232 // sync point.
233 if (EveryoneWaitingAtHint()) {
234 std::shared_ptr<const CueEvent> next_sync;
235 RETURN_IF_ERROR(GetNextCue(hint_, sync_points_, &next_sync));
236 RETURN_IF_ERROR(UseNewSyncPoint(next_sync));
237 }
238
239 return Status::OK;
240}
241
242Status CueAlignmentHandler::OnSample(std::unique_ptr<StreamData> sample) {
243 // There are two modes:
244 // 1. There is a video input.
245 // 2. There are no video inputs.
246 //
247 // When there is a video input, we rely on the video input get the next sync
248 // point and release all the samples.
249 //
250 // When there are no video inputs, we rely on the sync point queue to block
251 // us until there is a sync point.
252
253 const size_t stream_index = sample->stream_index;
254
255 if (sample->text_sample) {
256 StreamState& stream = stream_states_[stream_index];
257 stream.max_text_sample_end_time_seconds =
258 std::max(stream.max_text_sample_end_time_seconds,
259 TextEndTimeInSeconds(*stream.info, *sample));
260 }
261
262 const StreamType stream_type =
263 stream_states_[stream_index].info->stream_type();
264 const bool is_video = stream_type == kStreamVideo;
265
266 return is_video ? OnVideoSample(std::move(sample))
267 : OnNonVideoSample(std::move(sample));
268}
269
270Status CueAlignmentHandler::UseNewSyncPoint(
271 std::shared_ptr<const CueEvent> new_sync) {
272 hint_ = sync_points_->GetHint(new_sync->time_in_seconds);
273 DCHECK_GT(hint_, new_sync->time_in_seconds);
274
275 for (size_t stream_index = 0; stream_index < stream_states_.size();
276 stream_index++) {
277 StreamState& stream = stream_states_[stream_index];
278 stream.cues.push_back(StreamData::FromCueEvent(stream_index, new_sync));
279
280 RETURN_IF_ERROR(RunThroughSamples(&stream));
281 }
282
283 return Status::OK;
284}
285
286bool CueAlignmentHandler::EveryoneWaitingAtHint() const {
287 for (const StreamState& stream_state : stream_states_) {
288 if (stream_state.samples.empty()) {
289 return false;
290 }
291 }
292 return true;
293}
294
295Status CueAlignmentHandler::AcceptSample(std::unique_ptr<StreamData> sample,
296 StreamState* stream) {
297 DCHECK(sample);
298 DCHECK(sample->media_sample || sample->text_sample);
299 DCHECK(stream);
300
301 // Need to cache the stream index as we will lose the pointer when we add
302 // the sample to the queue.
303 const size_t stream_index = sample->stream_index;
304
305 stream->samples.push_back(std::move(sample));
306
307 if (stream->samples.size() > kMaxBufferSize) {
308 LOG(ERROR) << "Stream " << stream_index << " has buffered "
309 << stream->samples.size() << " when the max is "
310 << kMaxBufferSize;
311 return Status(error::INVALID_ARGUMENT,
312 "Streams are not properly multiplexed.");
313 }
314
315 return RunThroughSamples(stream);
316}
317
318Status CueAlignmentHandler::RunThroughSamples(StreamState* stream) {
319 // Step through all our samples until we find where we can insert the cue.
320 // Think of this as a merge sort.
321 while (stream->cues.size() && stream->samples.size()) {
322 const double cue_time = stream->cues.front()->cue_event->time_in_seconds;
323 const double sample_time =
324 TimeInSeconds(*stream->info, *stream->samples.front());
325
326 if (sample_time < cue_time) {
327 RETURN_IF_ERROR(Dispatch(std::move(stream->samples.front())));
328 stream->samples.pop_front();
329 } else {
330 RETURN_IF_ERROR(Dispatch(std::move(stream->cues.front())));
331 stream->cues.pop_front();
332 }
333 }
334
335 // If we still have samples, then it means that we sent out the cue and can
336 // now work up to the hint. So now send all samples that come before the hint
337 // downstream.
338 while (stream->samples.size() &&
339 TimeInSeconds(*stream->info, *stream->samples.front()) < hint_) {
340 RETURN_IF_ERROR(Dispatch(std::move(stream->samples.front())));
341 stream->samples.pop_front();
342 }
343
344 return Status::OK;
345}
346} // namespace media
347} // namespace shaka
All the methods that are virtual are virtual for mocking.