Shaka Packager SDK
Loading...
Searching...
No Matches
webvtt_to_mp4_handler.cc
1// Copyright 2017 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/webvtt/webvtt_to_mp4_handler.h>
8
9#include <algorithm>
10#include <cstddef>
11#include <cstdint>
12#include <list>
13#include <map>
14#include <memory>
15#include <utility>
16
17#include <absl/log/check.h>
18
19#include <packager/macros/logging.h>
20#include <packager/macros/status.h>
21#include <packager/media/base/buffer_writer.h>
22#include <packager/media/base/media_handler.h>
23#include <packager/media/base/media_sample.h>
24#include <packager/media/base/stream_info.h>
25#include <packager/media/base/text_sample.h>
26#include <packager/media/base/timestamp_util.h>
27#include <packager/media/formats/mp4/box_definitions.h>
28#include <packager/media/formats/webvtt/webvtt_utils.h>
29#include <packager/status.h>
30
31namespace shaka {
32namespace media {
33namespace {
34size_t kTrackId = 0;
35
36enum class DisplayActionType { ADD, REMOVE };
37
38struct DisplayAction {
39 DisplayActionType type;
40 const TextSample* sample;
41};
42
43std::multimap<int64_t, DisplayAction> CreateActionList(
44 int64_t segment_start,
45 int64_t segment_end,
46 const std::list<std::shared_ptr<const TextSample>>& samples) {
47 std::multimap<int64_t, DisplayAction> actions;
48
49 for (const auto& sample : samples) {
50 DCHECK(sample);
51
52 // The add action should occur either in this segment or in a previous
53 // segment. Use wrap-safe comparison since sample PTS may be wrapped
54 // but segment_end is unwrapped.
55 DCHECK(PtsIsBefore(sample->start_time(), segment_end))
56 << "Sample start " << sample->start_time()
57 << " should be before segment end " << segment_end;
58 actions.insert(
59 {sample->start_time(), {DisplayActionType::ADD, sample.get()}});
60
61 // If the remove happens in a later segment, then we don't want to include
62 // that action. Use wrap-safe comparison.
63 if (PtsIsBefore(sample->EndTime(), segment_end)) {
64 actions.insert(
65 {sample->EndTime(), {DisplayActionType::REMOVE, sample.get()}});
66 }
67 }
68
69 return actions;
70}
71
72void WriteSample(const TextSample& sample, BufferWriter* out) {
73 mp4::VTTCueBox box;
74
75 if (sample.id().length()) {
76 box.cue_id.cue_id = sample.id();
77 }
78 box.cue_settings.settings = WebVttSettingsToString(sample.settings());
79 box.cue_payload.cue_text = WebVttFragmentToString(sample.body());
80
81 // If there is internal timing, i.e. WebVTT cue timestamp, then
82 // cue_current_time should be populated
83 // "which gives the VTT timestamp associated with the start time of sample."
84 // TODO(rkuroiwa): Reuse TimestampToMilliseconds() to check if there is an
85 // internal timestamp in the payload to set CueTimeBox.cue_current_time.
86 box.Write(out);
87}
88
89void WriteSamples(const std::list<const TextSample*>& samples,
90 BufferWriter* writer) {
91 DCHECK_GE(samples.size(), 0u);
92
93 for (const auto& sample : samples) {
94 WriteSample(*sample, writer);
95 }
96}
97
98void WriteEmptySample(BufferWriter* writer) {
99 mp4::VTTEmptyCueBox box;
100 box.Write(writer);
101}
102
103std::shared_ptr<MediaSample> CreateMediaSample(const BufferWriter& buffer,
104 int64_t start_time,
105 int64_t end_time) {
106 DCHECK_GE(start_time, 0);
107 DCHECK_GT(end_time, start_time);
108
109 const bool kIsKeyFrame = true;
110
111 std::shared_ptr<MediaSample> sample =
112 MediaSample::CopyFrom(buffer.Buffer(), buffer.Size(), kIsKeyFrame);
113 sample->set_pts(start_time);
114 sample->set_dts(start_time);
115 sample->set_duration(end_time - start_time);
116
117 return sample;
118}
119} // namespace
120
121Status WebVttToMp4Handler::InitializeInternal() {
122 return Status::OK;
123}
124
125Status WebVttToMp4Handler::Process(std::unique_ptr<StreamData> stream_data) {
126 switch (stream_data->stream_data_type) {
127 case StreamDataType::kStreamInfo:
128 return OnStreamInfo(std::move(stream_data));
129 case StreamDataType::kCueEvent:
130 return OnCueEvent(std::move(stream_data));
131 case StreamDataType::kSegmentInfo:
132 return OnSegmentInfo(std::move(stream_data));
133 case StreamDataType::kTextSample:
134 return OnTextSample(std::move(stream_data));
135 default:
136 return Status(error::INTERNAL_ERROR,
137 "Invalid stream data type (" +
138 StreamDataTypeToString(stream_data->stream_data_type) +
139 ") for this WebVttToMp4 handler");
140 }
141}
142
143Status WebVttToMp4Handler::OnStreamInfo(
144 std::unique_ptr<StreamData> stream_data) {
145 DCHECK(stream_data);
146 DCHECK(stream_data->stream_info);
147
148 auto clone = stream_data->stream_info->Clone();
149 clone->set_codec(kCodecWebVtt);
150 clone->set_codec_string("wvtt");
151
152 if (clone->stream_type() != kStreamText) {
153 return Status(error::MUXER_FAILURE, "Incorrect stream type");
154 }
155
156 return Dispatch(
157 StreamData::FromStreamInfo(stream_data->stream_index, std::move(clone)));
158}
159
160Status WebVttToMp4Handler::OnCueEvent(std::unique_ptr<StreamData> stream_data) {
161 DCHECK(stream_data);
162 DCHECK(stream_data->cue_event);
163
164 if (current_segment_.size()) {
165 return Status(error::INTERNAL_ERROR,
166 "Cue Events should come right after segment info.");
167 }
168
169 return Dispatch(std::move(stream_data));
170}
171
172Status WebVttToMp4Handler::OnSegmentInfo(
173 std::unique_ptr<StreamData> stream_data) {
174 DCHECK(stream_data);
175 DCHECK(stream_data->segment_info);
176
177 const auto& segment = stream_data->segment_info;
178
179 int64_t segment_start = segment->start_timestamp;
180 int64_t segment_duration = segment->duration;
181 int64_t segment_end = segment_start + segment_duration;
182
183 RETURN_IF_ERROR(DispatchCurrentSegment(segment_start, segment_end));
184 current_segment_.clear();
185
186 return Dispatch(std::move(stream_data));
187}
188
189Status WebVttToMp4Handler::OnTextSample(
190 std::unique_ptr<StreamData> stream_data) {
191 DCHECK(stream_data);
192 DCHECK(stream_data->text_sample);
193
194 auto& sample = stream_data->text_sample;
195
196 // Ignore empty samples. This will create gaps, but we will handle that
197 // later.
198 if (sample->body().is_empty()) {
199 return Status::OK;
200 }
201
202 // Add the new text sample to the cache of samples that belong in the
203 // current segment.
204 current_segment_.push_back(std::move(stream_data->text_sample));
205 return Status::OK;
206}
207
208Status WebVttToMp4Handler::DispatchCurrentSegment(int64_t segment_start,
209 int64_t segment_end) {
210 // Active will hold all the samples that are "on screen" for the current
211 // section of time.
212 std::list<const TextSample*> active;
213
214 // Move through the segment, jumping between each change to the current state.
215 // A change is defined as a group of one or more DisplayActions.
216 int64_t section_start = segment_start;
217
218 // |actions| is a map of [time] -> [action].
219 auto actions = CreateActionList(segment_start, segment_end, current_segment_);
220 auto front = actions.begin();
221
222 // As it is possible to have a segment with no samples, we can't base this
223 // loop on the number of actions. So we need to keep iterating until we
224 // have written enough sections to get to the end of the segment.
225 while (section_start < segment_end) {
226 // Apply all actions that occur at the start of this part of the segment.
227 // Normally we would only want "== section_start" but as it is possible for
228 // samples to span multiple segments, their start time will be before the
229 // segment's start time. So we want to apply them too if they come before
230 // the segment. Thus why we use "<=".
231 while (front != actions.end() && front->first <= section_start) {
232 auto& action = front->second;
233
234 switch (action.type) {
235 case DisplayActionType::ADD: {
236 active.push_back(action.sample);
237 break;
238 }
239 case DisplayActionType::REMOVE: {
240 auto found = std::find(active.begin(), active.end(), action.sample);
241 DCHECK(found != active.end());
242 active.erase(found);
243 break;
244 }
245 default: {
246 NOTIMPLEMENTED() << "Unsupported DisplayActionType "
247 << static_cast<int>(action.type);
248 break;
249 }
250 }
251
252 // We have "consumed" the action at the front. We can move on.
253 front++;
254 }
255
256 // The end of the section will either be the start of the next section or
257 // the end of the segment.
258 int64_t section_end = front == actions.end() ? segment_end : front->first;
259 DCHECK_GT(section_end, section_start);
260 DCHECK_LE(section_end, segment_end);
261 RETURN_IF_ERROR(MergeDispatchSamples(section_start, section_end, active));
262
263 section_start = section_end;
264 }
265
266 DCHECK(front == actions.end()) << "We should have processed all actions.";
267
268 return Status::OK;
269}
270
271Status WebVttToMp4Handler::MergeDispatchSamples(
272 int64_t start_time,
273 int64_t end_time,
274 const std::list<const TextSample*>& state) {
275 DCHECK_GT(end_time, start_time);
276
277 box_writer_.Clear();
278
279 if (state.size()) {
280 WriteSamples(state, &box_writer_);
281 } else {
282 WriteEmptySample(&box_writer_);
283 }
284
285 return DispatchMediaSample(
286 kTrackId, CreateMediaSample(box_writer_, start_time, end_time));
287}
288} // namespace media
289} // namespace shaka
Status DispatchMediaSample(size_t stream_index, std::shared_ptr< const MediaSample > media_sample) const
Dispatch the media sample to downstream handlers.
Status Dispatch(std::unique_ptr< StreamData > stream_data) const
static std::shared_ptr< MediaSample > CopyFrom(const uint8_t *data, size_t size, bool is_key_frame)
All the methods that are virtual are virtual for mocking.