blob: f49d151661f72d93836cc405b19f6737d57012dc [file] [log] [blame]
/*
* Copyright (C) 2013 Google Inc. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// Modifications Copyright 2017 The Cobalt Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cobalt/dom/source_buffer.h"
#include <algorithm>
#include <limits>
#include <utility>
#include "base/compiler_specific.h"
#include "base/logging.h"
#include "base/message_loop/message_loop.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "cobalt/base/polymorphic_downcast.h"
#include "cobalt/base/tokens.h"
#include "cobalt/dom/dom_settings.h"
#include "cobalt/dom/media_source.h"
#include "cobalt/web/dom_exception.h"
#include "third_party/chromium/media/base/ranges.h"
#include "third_party/chromium/media/base/timestamp_constants.h"
namespace cobalt {
namespace dom {
namespace {
static base::TimeDelta DoubleToTimeDelta(double time) {
DCHECK(!std::isnan(time));
DCHECK_NE(time, -std::numeric_limits<double>::infinity());
if (time == std::numeric_limits<double>::infinity()) {
return ::media::kInfiniteDuration;
}
// Don't use base::TimeDelta::Max() here, as we want the largest finite time
// delta.
base::TimeDelta max_time = base::TimeDelta::FromInternalValue(
std::numeric_limits<int64_t>::max() - 1);
double max_time_in_seconds = max_time.InSecondsF();
if (time >= max_time_in_seconds) {
return max_time;
}
return base::TimeDelta::FromSecondsD(time);
}
// The return value will be used in `SourceBuffer::EvictCodedFrames()` to allow
// it to evict extra data from the SourceBuffer, so it can reduce the overall
// memory used by the underlying Demuxer implementation.
// The default value is 0, i.e. do not evict extra bytes.
size_t GetEvictExtraInBytes(script::EnvironmentSettings* settings) {
DOMSettings* dom_settings =
base::polymorphic_downcast<DOMSettings*>(settings);
DCHECK(dom_settings);
DCHECK(dom_settings->media_source_settings());
int bytes = dom_settings->media_source_settings()
->GetSourceBufferEvictExtraInBytes()
.value_or(0);
DCHECK_GE(bytes, 0);
return std::max<int>(bytes, 0);
}
} // namespace
SourceBuffer::SourceBuffer(script::EnvironmentSettings* settings,
const std::string& id, MediaSource* media_source,
bool asynchronous_reduction_enabled,
ChunkDemuxer* chunk_demuxer, EventQueue* event_queue)
: web::EventTarget(settings),
id_(id),
asynchronous_reduction_enabled_(asynchronous_reduction_enabled),
evict_extra_in_bytes_(GetEvictExtraInBytes(settings)),
media_source_(media_source),
chunk_demuxer_(chunk_demuxer),
event_queue_(event_queue),
audio_tracks_(
new AudioTrackList(settings, media_source->GetMediaElement())),
video_tracks_(
new VideoTrackList(settings, media_source->GetMediaElement())),
metrics_(!media_source_->MediaElementHasMaxVideoCapabilities()) {
DCHECK(!id_.empty());
DCHECK(media_source_);
DCHECK(chunk_demuxer);
DCHECK(event_queue);
LOG(INFO) << "Evict extra in bytes is set to " << evict_extra_in_bytes_;
chunk_demuxer_->SetTracksWatcher(
id_,
base::Bind(&SourceBuffer::OnInitSegmentReceived, base::Unretained(this)));
chunk_demuxer_->SetParseWarningCallback(
id, base::BindRepeating([](::media::SourceBufferParseWarning warning) {
LOG(WARNING) << "Encountered SourceBufferParseWarning "
<< static_cast<int>(warning);
}));
}
void SourceBuffer::set_mode(SourceBufferAppendMode mode,
script::ExceptionState* exception_state) {
if (media_source_ == NULL) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return;
}
if (updating()) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return;
}
media_source_->OpenIfInEndedState();
if (chunk_demuxer_->IsParsingMediaSegment(id_)) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return;
}
chunk_demuxer_->SetSequenceMode(id_, mode == kSourceBufferAppendModeSequence);
mode_ = mode;
}
scoped_refptr<TimeRanges> SourceBuffer::buffered(
script::ExceptionState* exception_state) const {
if (media_source_ == NULL) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return NULL;
}
scoped_refptr<TimeRanges> time_ranges = new TimeRanges;
::media::Ranges<base::TimeDelta> ranges =
chunk_demuxer_->GetBufferedRanges(id_);
for (size_t i = 0; i < ranges.size(); i++) {
time_ranges->Add(ranges.start(i).InSecondsF(), ranges.end(i).InSecondsF());
}
return time_ranges;
}
double SourceBuffer::timestamp_offset(
script::ExceptionState* exception_state) const {
starboard::ScopedLock scoped_lock(timestamp_offset_mutex_);
return timestamp_offset_;
}
void SourceBuffer::set_timestamp_offset(
double offset, script::ExceptionState* exception_state) {
if (media_source_ == NULL) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return;
}
if (updating()) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return;
}
media_source_->OpenIfInEndedState();
if (chunk_demuxer_->IsParsingMediaSegment(id_)) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return;
}
// We don't have to acquire |timestamp_offset_mutex_|, as no algorithms are
// running asynchronously at this moment, which is guaranteed by the check of
// updating() above.
timestamp_offset_ = offset;
chunk_demuxer_->SetGroupStartTimestampIfInSequenceMode(
id_, DoubleToTimeDelta(timestamp_offset_));
}
void SourceBuffer::set_append_window_start(
double start, script::ExceptionState* exception_state) {
if (media_source_ == NULL) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return;
}
if (updating()) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return;
}
if (start < 0 || start >= append_window_end_) {
exception_state->SetSimpleException(script::kSimpleTypeError);
return;
}
append_window_start_ = start;
}
void SourceBuffer::set_append_window_end(
double end, script::ExceptionState* exception_state) {
if (media_source_ == NULL) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return;
}
if (updating()) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return;
}
if (std::isnan(end)) {
exception_state->SetSimpleException(script::kSimpleTypeError);
return;
}
// 4. If the new value is less than or equal to appendWindowStart then throw a
// TypeError exception and abort these steps.
if (end <= append_window_start_) {
exception_state->SetSimpleException(script::kSimpleTypeError);
return;
}
append_window_end_ = end;
}
void SourceBuffer::AppendBuffer(const script::Handle<script::ArrayBuffer>& data,
script::ExceptionState* exception_state) {
TRACE_EVENT1("cobalt::dom", "SourceBuffer::AppendBuffer()", "size",
data->ByteLength());
AppendBufferInternal(static_cast<const unsigned char*>(data->Data()),
data->ByteLength(), exception_state);
}
void SourceBuffer::AppendBuffer(
const script::Handle<script::ArrayBufferView>& data,
script::ExceptionState* exception_state) {
TRACE_EVENT1("cobalt::dom", "SourceBuffer::AppendBuffer()", "size",
data->ByteLength());
AppendBufferInternal(static_cast<const unsigned char*>(data->RawData()),
data->ByteLength(), exception_state);
}
void SourceBuffer::Abort(script::ExceptionState* exception_state) {
TRACE_EVENT0("cobalt::dom", "SourceBuffer::Abort()");
if (media_source_ == NULL) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return;
}
if (!media_source_->IsOpen()) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return;
}
if (active_algorithm_handle_) {
if (!active_algorithm_handle_->algorithm()->SupportExplicitAbort()) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return;
}
active_algorithm_handle_->Abort();
active_algorithm_handle_ = nullptr;
}
base::TimeDelta timestamp_offset = DoubleToTimeDelta(timestamp_offset_);
chunk_demuxer_->ResetParserState(id_, DoubleToTimeDelta(append_window_start_),
DoubleToTimeDelta(append_window_end_),
&timestamp_offset);
UpdateTimestampOffset(timestamp_offset);
set_append_window_start(0, exception_state);
set_append_window_end(std::numeric_limits<double>::infinity(),
exception_state);
}
void SourceBuffer::Remove(double start, double end,
script::ExceptionState* exception_state) {
TRACE_EVENT2("cobalt::dom", "SourceBuffer::Remove()", "start", start, "end",
end);
if (media_source_ == NULL) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return;
}
if (updating()) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return;
}
if (start < 0 || std::isnan(media_source_->duration(NULL)) ||
start > media_source_->duration(NULL)) {
exception_state->SetSimpleException(script::kSimpleTypeError);
return;
}
if (end <= start || std::isnan(end)) {
exception_state->SetSimpleException(script::kSimpleTypeError);
return;
}
media_source_->OpenIfInEndedState();
ScheduleEvent(base::Tokens::updatestart());
DCHECK_GE(start, 0);
DCHECK_LT(start, end);
std::unique_ptr<SourceBufferAlgorithm> algorithm(
new SourceBufferRemoveAlgorithm(
chunk_demuxer_, id_, DoubleToTimeDelta(start), DoubleToTimeDelta(end),
base::Bind(asynchronous_reduction_enabled_
? &SourceBuffer::ScheduleAndMaybeDispatchImmediately
: &SourceBuffer::ScheduleEvent,
base::Unretained(this)),
base::Bind(&SourceBuffer::OnAlgorithmFinalized,
base::Unretained(this))));
active_algorithm_handle_ =
new SerializedAlgorithmRunner::Handle<SourceBufferAlgorithm>(
std::move(algorithm));
media_source_->GetAlgorithmRunner(std::numeric_limits<int>::max())
->Start(active_algorithm_handle_);
}
void SourceBuffer::set_track_defaults(
const scoped_refptr<TrackDefaultList>& track_defaults,
script::ExceptionState* exception_state) {
if (media_source_ == NULL) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return;
}
if (updating()) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return;
}
track_defaults_ = track_defaults;
}
void SourceBuffer::OnRemovedFromMediaSource() {
if (media_source_ == NULL) {
return;
}
if (active_algorithm_handle_) {
active_algorithm_handle_->Abort();
active_algorithm_handle_ = nullptr;
}
DCHECK(media_source_);
// TODO: Implement track support.
// if (media_source_->GetMediaElement()->audio_tracks().length() > 0 ||
// media_source_->GetMediaElement()->audio_tracks().length() > 0) {
// RemoveMediaTracks();
// }
// TODO: Determine if the source buffer contains an audio or video stream,
// maybe by implementing track support and get from the type of the
// track, and print the steam type along with the metrics.
metrics_.PrintCurrentMetricsAndUpdateAccumulatedMetrics();
chunk_demuxer_->RemoveId(id_);
if (chunk_demuxer_->GetAllStreams().empty()) {
metrics_.PrintAccumulatedMetrics();
}
chunk_demuxer_ = NULL;
media_source_ = NULL;
event_queue_ = NULL;
pending_append_data_.reset();
pending_append_data_capacity_ = 0;
}
double SourceBuffer::GetHighestPresentationTimestamp() const {
DCHECK(media_source_ != NULL);
return chunk_demuxer_->GetHighestPresentationTimestamp(id_).InSecondsF();
}
void SourceBuffer::TraceMembers(script::Tracer* tracer) {
web::EventTarget::TraceMembers(tracer);
tracer->Trace(event_queue_);
tracer->Trace(media_source_);
tracer->Trace(track_defaults_);
tracer->Trace(audio_tracks_);
tracer->Trace(video_tracks_);
}
void SourceBuffer::OnInitSegmentReceived(std::unique_ptr<MediaTracks> tracks) {
if (!first_initialization_segment_received_) {
// This can be called from non-web thread when the append is async.
if (!web_task_runner_->BelongsToCurrentThread()) {
web_task_runner_->PostTask(
FROM_HERE, base::Bind(&SourceBuffer::OnInitSegmentReceived, this,
base::Passed(&tracks)));
return;
}
media_source_->SetSourceBufferActive(this, true);
first_initialization_segment_received_ = true;
}
// TODO: Implement track support.
}
void SourceBuffer::ScheduleEvent(base::Token event_name) {
scoped_refptr<web::Event> event = new web::Event(event_name);
event->set_target(this);
event_queue_->Enqueue(event);
}
void SourceBuffer::ScheduleAndMaybeDispatchImmediately(base::Token event_name) {
scoped_refptr<web::Event> event = new web::Event(event_name);
event->set_target(this);
event_queue_->EnqueueAndMaybeDispatchImmediately(event);
}
bool SourceBuffer::PrepareAppend(size_t new_data_size,
script::ExceptionState* exception_state) {
TRACE_EVENT1("cobalt::dom", "SourceBuffer::PrepareAppend()", "new_data_size",
new_data_size);
if (media_source_ == NULL) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return false;
}
if (updating()) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return false;
}
DCHECK(media_source_->GetMediaElement());
if (media_source_->GetMediaElement()->error()) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return false;
}
media_source_->OpenIfInEndedState();
double current_time = media_source_->GetMediaElement()->current_time(NULL);
if (!chunk_demuxer_->EvictCodedFrames(
id_, base::TimeDelta::FromSecondsD(current_time),
new_data_size + evict_extra_in_bytes_)) {
web::DOMException::Raise(web::DOMException::kQuotaExceededErr,
exception_state);
return false;
}
return true;
}
void SourceBuffer::AppendBufferInternal(
const unsigned char* data, size_t size,
script::ExceptionState* exception_state) {
TRACE_EVENT1("cobalt::dom", "SourceBuffer::AppendBufferInternal()", "size",
size);
metrics_.StartTracking();
if (!PrepareAppend(size, exception_state)) {
return;
}
metrics_.EndTracking(0);
DCHECK(data || size == 0);
if (data) {
if (pending_append_data_capacity_ < size) {
pending_append_data_.reset();
pending_append_data_.reset(new uint8_t[size]);
pending_append_data_capacity_ = size;
}
memcpy(pending_append_data_.get(), data, size);
}
ScheduleEvent(base::Tokens::updatestart());
std::unique_ptr<SourceBufferAlgorithm> algorithm(
new SourceBufferAppendAlgorithm(
media_source_, chunk_demuxer_, id_, pending_append_data_.get(), size,
DoubleToTimeDelta(append_window_start_),
DoubleToTimeDelta(append_window_end_),
DoubleToTimeDelta(timestamp_offset_),
base::Bind(&SourceBuffer::UpdateTimestampOffset,
base::Unretained(this)),
base::Bind(asynchronous_reduction_enabled_
? &SourceBuffer::ScheduleAndMaybeDispatchImmediately
: &SourceBuffer::ScheduleEvent,
base::Unretained(this)),
base::Bind(&SourceBuffer::OnAlgorithmFinalized,
base::Unretained(this)),
&metrics_));
active_algorithm_handle_ =
new SerializedAlgorithmRunner::Handle<SourceBufferAlgorithm>(
std::move(algorithm));
media_source_->GetAlgorithmRunner(size)->Start(active_algorithm_handle_);
}
void SourceBuffer::OnAlgorithmFinalized() {
DCHECK(active_algorithm_handle_);
active_algorithm_handle_ = nullptr;
}
void SourceBuffer::UpdateTimestampOffset(base::TimeDelta timestamp_offset) {
starboard::ScopedLock scoped_lock(timestamp_offset_mutex_);
// The check avoids overwriting |timestamp_offset_| when there is a small
// difference between its float and its int64_t representation .
if (DoubleToTimeDelta(timestamp_offset_) != timestamp_offset) {
timestamp_offset_ = timestamp_offset.InSecondsF();
}
}
} // namespace dom
} // namespace cobalt