blob: a970978f0a4c9fb09dab4d2475575b17a5c36b12 [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/media_source.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <vector>
#include "base/compiler_specific.h"
#include "base/guid.h"
#include "base/logging.h"
#include "cobalt/base/tokens.h"
#include "cobalt/dom/dom_exception.h"
#include "cobalt/dom/dom_settings.h"
#include "cobalt/dom/event.h"
#include "cobalt/media/base/pipeline_status.h"
#include "starboard/media.h"
namespace cobalt {
namespace dom {
using media::CHUNK_DEMUXER_ERROR_EOS_STATUS_DECODE_ERROR;
using media::CHUNK_DEMUXER_ERROR_EOS_STATUS_NETWORK_ERROR;
using media::PIPELINE_OK;
using media::PipelineStatus;
MediaSource::MediaSource(script::EnvironmentSettings* settings)
: EventTarget(settings),
chunk_demuxer_(NULL),
ready_state_(kMediaSourceReadyStateClosed),
ALLOW_THIS_IN_INITIALIZER_LIST(event_queue_(this)),
source_buffers_(new SourceBufferList(settings, &event_queue_)),
active_source_buffers_(new SourceBufferList(settings, &event_queue_)),
live_seekable_range_(new TimeRanges) {}
MediaSource::~MediaSource() { SetReadyState(kMediaSourceReadyStateClosed); }
scoped_refptr<SourceBufferList> MediaSource::source_buffers() const {
return source_buffers_;
}
scoped_refptr<SourceBufferList> MediaSource::active_source_buffers() const {
return active_source_buffers_;
}
MediaSourceReadyState MediaSource::ready_state() const { return ready_state_; }
double MediaSource::duration(script::ExceptionState* exception_state) const {
if (ready_state_ == kMediaSourceReadyStateClosed) {
return std::numeric_limits<float>::quiet_NaN();
}
DCHECK(chunk_demuxer_);
return chunk_demuxer_->GetDuration();
}
void MediaSource::set_duration(double duration,
script::ExceptionState* exception_state) {
if (duration < 0.0 || std::isnan(duration)) {
DOMException::Raise(DOMException::kIndexSizeErr, exception_state);
return;
}
if (!IsOpen() || IsUpdating()) {
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
return;
}
// Run the duration change algorithm
if (duration == this->duration(NULL)) {
return;
}
double highest_buffered_presentation_timestamp = 0;
for (uint32 i = 0; i < source_buffers_->length(); ++i) {
highest_buffered_presentation_timestamp =
std::max(highest_buffered_presentation_timestamp,
source_buffers_->Item(i)->GetHighestPresentationTimestamp());
}
if (duration < highest_buffered_presentation_timestamp) {
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
return;
}
// 3. Set old duration to the current value of duration.
double old_duration = this->duration(NULL);
DCHECK_LE(highest_buffered_presentation_timestamp,
std::isnan(old_duration) ? 0 : old_duration);
// 4. Update duration to new duration.
bool request_seek = attached_element_->current_time(NULL) > duration;
chunk_demuxer_->SetDuration(duration);
// 5. If a user agent is unable to partially render audio frames or text cues
// that start before and end after the duration, then run the following
// steps:
// NOTE: Currently we assume that the media engine is able to render
// partial frames/cues. If a media engine gets added that doesn't support
// this, then we'll need to add logic to handle the substeps.
// 6. Update the media controller duration to new duration and run the
// HTMLMediaElement duration change algorithm.
attached_element_->DurationChanged(duration, request_seek);
}
scoped_refptr<SourceBuffer> MediaSource::AddSourceBuffer(
script::EnvironmentSettings* settings, const std::string& type,
script::ExceptionState* exception_state) {
DLOG(INFO) << "add SourceBuffer with type " << type;
if (type.empty()) {
DOMException::Raise(DOMException::kInvalidAccessErr, exception_state);
// Return value should be ignored.
return NULL;
}
if (!IsTypeSupported(settings, type)) {
DOMException::Raise(DOMException::kNotSupportedErr, exception_state);
return NULL;
}
if (!IsOpen()) {
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
return NULL;
}
std::string guid = base::GenerateGUID();
scoped_refptr<SourceBuffer> source_buffer;
ChunkDemuxer::Status status = chunk_demuxer_->AddId(guid, type);
switch (status) {
case ChunkDemuxer::kOk:
source_buffer =
new SourceBuffer(settings, guid, this, chunk_demuxer_, &event_queue_);
break;
case ChunkDemuxer::kNotSupported:
DOMException::Raise(DOMException::kNotSupportedErr, exception_state);
return NULL;
case ChunkDemuxer::kReachedIdLimit:
DOMException::Raise(DOMException::kQuotaExceededErr, exception_state);
return NULL;
}
DCHECK(source_buffer);
source_buffers_->Add(source_buffer);
return source_buffer;
}
void MediaSource::RemoveSourceBuffer(
const scoped_refptr<SourceBuffer>& source_buffer,
script::ExceptionState* exception_state) {
if (source_buffer.get() == NULL) {
DOMException::Raise(DOMException::kInvalidAccessErr, exception_state);
return;
}
if (source_buffers_->length() == 0 ||
!source_buffers_->Contains(source_buffer)) {
DOMException::Raise(DOMException::kNotFoundErr, exception_state);
return;
}
source_buffer->OnRemovedFromMediaSource();
active_source_buffers_->Remove(source_buffer);
source_buffers_->Remove(source_buffer);
}
void MediaSource::EndOfStreamAlgorithm(MediaSourceEndOfStreamError error) {
SetReadyState(kMediaSourceReadyStateEnded);
PipelineStatus pipeline_status = PIPELINE_OK;
if (error == kMediaSourceEndOfStreamErrorNetwork) {
pipeline_status = CHUNK_DEMUXER_ERROR_EOS_STATUS_NETWORK_ERROR;
} else if (error == kMediaSourceEndOfStreamErrorDecode) {
pipeline_status = CHUNK_DEMUXER_ERROR_EOS_STATUS_DECODE_ERROR;
}
chunk_demuxer_->MarkEndOfStream(pipeline_status);
}
void MediaSource::EndOfStream(script::ExceptionState* exception_state) {
// If there is no error string provided, treat it as empty.
EndOfStream(kMediaSourceEndOfStreamErrorNoError, exception_state);
}
void MediaSource::EndOfStream(MediaSourceEndOfStreamError error,
script::ExceptionState* exception_state) {
if (!IsOpen() || IsUpdating()) {
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
return;
}
EndOfStreamAlgorithm(error);
}
void MediaSource::SetLiveSeekableRange(
double start, double end, script::ExceptionState* exception_state) {
if (!IsOpen()) {
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
return;
}
if (start < 0 || start > end) {
DOMException::Raise(DOMException::kIndexSizeErr, exception_state);
return;
}
live_seekable_range_ = new TimeRanges(start, end);
}
void MediaSource::ClearLiveSeekableRange(
script::ExceptionState* exception_state) {
if (!IsOpen()) {
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
return;
}
if (live_seekable_range_->length() != 0) {
live_seekable_range_ = new TimeRanges;
}
}
// static
bool MediaSource::IsTypeSupported(script::EnvironmentSettings* settings,
const std::string& type) {
DCHECK(settings);
DOMSettings* dom_settings =
base::polymorphic_downcast<DOMSettings*>(settings);
DCHECK(dom_settings->can_play_type_handler());
SbMediaSupportType support_type =
dom_settings->can_play_type_handler()->CanPlayAdaptive(type.c_str(), "");
if (support_type == kSbMediaSupportTypeNotSupported) {
LOG(INFO) << "MediaSource::IsTypeSupported(" << type
<< ") -> not supported/false";
return false;
}
if (support_type == kSbMediaSupportTypeMaybe) {
LOG(INFO) << "MediaSource::IsTypeSupported(" << type << ") -> maybe/true";
return true;
}
if (support_type == kSbMediaSupportTypeProbably) {
LOG(INFO) << "MediaSource::IsTypeSupported(" << type
<< ") -> probably/true";
return true;
}
NOTREACHED();
return false;
}
bool MediaSource::AttachToElement(HTMLMediaElement* media_element) {
if (attached_element_) {
return false;
}
DCHECK(IsClosed());
attached_element_ = base::AsWeakPtr(media_element);
return true;
}
void MediaSource::SetChunkDemuxerAndOpen(ChunkDemuxer* chunk_demuxer) {
DCHECK(chunk_demuxer);
DCHECK(!chunk_demuxer_);
DCHECK(attached_element_);
chunk_demuxer_ = chunk_demuxer;
SetReadyState(kMediaSourceReadyStateOpen);
}
void MediaSource::Close() { SetReadyState(kMediaSourceReadyStateClosed); }
bool MediaSource::IsClosed() const {
return ready_state_ == kMediaSourceReadyStateClosed;
}
scoped_refptr<TimeRanges> MediaSource::GetBufferedRange() const {
std::vector<scoped_refptr<TimeRanges> > ranges(
active_source_buffers_->length());
for (uint32 i = 0; i < active_source_buffers_->length(); ++i)
ranges[i] = active_source_buffers_->Item(i)->buffered(NULL);
if (ranges.empty()) {
return new TimeRanges;
}
double highest_end_time = -1;
for (size_t i = 0; i < ranges.size(); ++i) {
uint32 length = ranges[i]->length();
if (length > 0) {
highest_end_time =
std::max(highest_end_time, ranges[i]->End(length - 1, NULL));
}
}
// Return an empty range if all ranges are empty.
if (highest_end_time < 0) {
return new TimeRanges;
}
scoped_refptr<TimeRanges> intersection_ranges =
new TimeRanges(0, highest_end_time);
bool ended = ready_state() == kMediaSourceReadyStateEnded;
for (size_t i = 0; i < ranges.size(); ++i) {
scoped_refptr<TimeRanges> source_ranges = ranges[i].get();
if (ended && source_ranges->length()) {
source_ranges->Add(
source_ranges->Start(source_ranges->length() - 1, NULL),
highest_end_time);
}
intersection_ranges = intersection_ranges->IntersectWith(source_ranges);
}
return intersection_ranges;
}
scoped_refptr<TimeRanges> MediaSource::GetSeekable() const {
// Implements MediaSource algorithm for HTMLMediaElement.seekable.
double source_duration = duration(NULL);
if (std::isnan(source_duration)) {
return new TimeRanges;
}
if (source_duration == std::numeric_limits<double>::infinity()) {
scoped_refptr<TimeRanges> buffered = attached_element_->buffered();
if (live_seekable_range_->length() != 0) {
if (buffered->length() == 0) {
return new TimeRanges(live_seekable_range_->Start(0, NULL),
live_seekable_range_->End(0, NULL));
}
return new TimeRanges(
std::min(live_seekable_range_->Start(0, NULL),
buffered->Start(0, NULL)),
std::max(live_seekable_range_->End(0, NULL),
buffered->End(buffered->length() - 1, NULL)));
}
if (buffered->length() == 0) {
return new TimeRanges;
}
return new TimeRanges(0, buffered->End(buffered->length() - 1, NULL));
}
return new TimeRanges(0, source_duration);
}
void MediaSource::OnAudioTrackChanged(AudioTrack* audio_track) {
scoped_refptr<SourceBuffer> source_buffer = audio_track->source_buffer();
if (!source_buffer) {
return;
}
DCHECK(source_buffers_->Contains(source_buffer));
source_buffer->audio_tracks()->ScheduleChangeEvent();
bool is_active = (source_buffer->video_tracks()->selected_index() != -1) ||
source_buffer->audio_tracks()->HasEnabledTrack();
SetSourceBufferActive(source_buffer, is_active);
}
void MediaSource::OnVideoTrackChanged(VideoTrack* video_track) {
scoped_refptr<SourceBuffer> source_buffer = video_track->source_buffer();
if (!source_buffer) {
return;
}
DCHECK(source_buffers_->Contains(source_buffer));
if (video_track->selected()) {
source_buffer->video_tracks()->OnTrackSelected(video_track->id());
}
source_buffer->video_tracks()->ScheduleChangeEvent();
bool is_active = source_buffer->video_tracks()->selected_index() != -1 ||
source_buffer->audio_tracks()->HasEnabledTrack();
SetSourceBufferActive(source_buffer, is_active);
}
void MediaSource::OpenIfInEndedState() {
if (ready_state_ != kMediaSourceReadyStateEnded) {
return;
}
SetReadyState(kMediaSourceReadyStateOpen);
chunk_demuxer_->UnmarkEndOfStream();
}
bool MediaSource::IsOpen() const {
return ready_state_ == kMediaSourceReadyStateOpen;
}
void MediaSource::SetSourceBufferActive(SourceBuffer* source_buffer,
bool is_active) {
// We don't support deactivate a source buffer.
DCHECK(is_active);
DCHECK(source_buffers_->Contains(source_buffer));
if (!is_active) {
DCHECK(active_source_buffers_->Contains(source_buffer));
active_source_buffers_->Remove(source_buffer);
return;
}
if (active_source_buffers_->Contains(source_buffer)) {
return;
}
size_t index = source_buffers_->Find(source_buffer);
uint32 insert_position = 0;
while (insert_position < active_source_buffers_->length() &&
source_buffers_->Find(active_source_buffers_->Item(insert_position)) <
index) {
++insert_position;
}
active_source_buffers_->Insert(insert_position, source_buffer);
}
HTMLMediaElement* MediaSource::GetMediaElement() const {
return attached_element_;
}
void MediaSource::TraceMembers(script::Tracer* tracer) {
EventTarget::TraceMembers(tracer);
tracer->Trace(event_queue_);
tracer->Trace(attached_element_);
tracer->Trace(source_buffers_);
tracer->Trace(active_source_buffers_);
tracer->Trace(live_seekable_range_);
}
void MediaSource::SetReadyState(MediaSourceReadyState ready_state) {
if (ready_state == kMediaSourceReadyStateClosed) {
chunk_demuxer_ = NULL;
}
if (ready_state_ == ready_state) {
return;
}
MediaSourceReadyState old_state = ready_state_;
ready_state_ = ready_state;
if (IsOpen()) {
ScheduleEvent(base::Tokens::sourceopen());
return;
}
if (old_state == kMediaSourceReadyStateOpen &&
ready_state_ == kMediaSourceReadyStateEnded) {
ScheduleEvent(base::Tokens::sourceended());
return;
}
DCHECK(IsClosed());
active_source_buffers_->Clear();
// Clear SourceBuffer references to this object.
for (uint32 i = 0; i < source_buffers_->length(); ++i) {
source_buffers_->Item(i)->OnRemovedFromMediaSource();
}
source_buffers_->Clear();
attached_element_.reset();
ScheduleEvent(base::Tokens::sourceclose());
}
bool MediaSource::IsUpdating() const {
// Return true if any member of |source_buffers_| is updating.
for (uint32 i = 0; i < source_buffers_->length(); ++i) {
if (source_buffers_->Item(i)->updating()) {
return true;
}
}
return false;
}
void MediaSource::ScheduleEvent(base::Token event_name) {
scoped_refptr<Event> event = new Event(event_name);
event->set_target(this);
event_queue_.Enqueue(event);
}
} // namespace dom
} // namespace cobalt