blob: c87fdb5f8a8a6271fe9d6bcbfeb2d0c66688b77c [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 Google Inc. 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/media_source.h"
#include <algorithm>
#include <limits>
#include <vector>
#include "base/compiler_specific.h"
#include "base/guid.h"
#include "base/logging.h"
#include "base/string_split.h"
#include "base/string_util.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 {
#if defined(COBALT_MEDIA_SOURCE_2016)
using media::PipelineStatus;
using media::CHUNK_DEMUXER_ERROR_EOS_STATUS_NETWORK_ERROR;
using media::CHUNK_DEMUXER_ERROR_EOS_STATUS_DECODE_ERROR;
using media::PIPELINE_OK;
#else // defined(COBALT_MEDIA_SOURCE_2016)
using ::media::PipelineStatus;
using ::media::CHUNK_DEMUXER_ERROR_EOS_STATUS_NETWORK_ERROR;
using ::media::CHUNK_DEMUXER_ERROR_EOS_STATUS_DECODE_ERROR;
using ::media::PIPELINE_OK;
#endif // defined(COBALT_MEDIA_SOURCE_2016)
namespace {
// Parse mime and codecs from content type. It will return "video/mp4" and
// "avc1.42E01E, mp4a.40.2" for "video/mp4; codecs="avc1.42E01E, mp4a.40.2".
// Note that this function does minimum validation as the media stack will check
// the mime type and codecs strictly.
bool ParseContentType(const std::string& content_type, std::string* mime,
std::string* codecs) {
DCHECK(mime);
DCHECK(codecs);
static const char kCodecs[] = "codecs=";
std::vector<std::string> tokens;
// SplitString will also trim the results.
::base::SplitString(content_type, ';', &tokens);
// The first one has to be mime type with delimiter '/' like 'video/mp4'.
if (tokens.size() < 2 || tokens[0].find('/') == tokens[0].npos) {
return false;
}
*mime = tokens[0];
for (size_t i = 1; i < tokens.size(); ++i) {
if (base::strncasecmp(tokens[i].c_str(), kCodecs, strlen(kCodecs))) {
continue;
}
*codecs = tokens[i].substr(strlen("codecs="));
TrimString(*codecs, " \"", codecs);
break;
}
return !codecs->empty();
}
} // namespace
MediaSource::MediaSource()
: chunk_demuxer_(NULL),
ready_state_(kMediaSourceReadyStateClosed),
ALLOW_THIS_IN_INITIALIZER_LIST(event_queue_(this)),
source_buffers_(new SourceBufferList(&event_queue_)),
active_source_buffers_(new SourceBufferList(&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 {
UNREFERENCED_PARAMETER(exception_state);
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 || 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(
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(NULL, type)) {
DOMException::Raise(DOMException::kNotSupportedErr, exception_state);
return NULL;
}
if (!IsOpen()) {
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
return NULL;
}
std::string mime;
std::string codecs;
if (!ParseContentType(type, &mime, &codecs)) {
DOMException::Raise(DOMException::kNotSupportedErr, exception_state);
// Return value should be ignored.
return NULL;
}
std::string guid = base::GenerateGUID();
scoped_refptr<SourceBuffer> source_buffer;
ChunkDemuxer::Status status = chunk_demuxer_->AddId(guid, mime, codecs);
switch (status) {
case ChunkDemuxer::kOk:
source_buffer =
new SourceBuffer(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 == 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::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;
}
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::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) {
// TODO: Remove |settings| parameter once MSE2012 is removed.
UNREFERENCED_PARAMETER(settings);
SbMediaSupportType support_type =
SbMediaCanPlayMimeAndKeySystem(type.c_str(), "");
if (support_type == kSbMediaSupportTypeNotSupported) {
DLOG(INFO) << "MediaSource::IsTypeSupported(" << type
<< ") -> not supported/false";
return false;
}
if (support_type == kSbMediaSupportTypeMaybe) {
DLOG(INFO) << "MediaSource::IsTypeSupported(" << type << ") -> maybe/true";
return true;
}
if (support_type == kSbMediaSupportTypeProbably) {
DLOG(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::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