blob: d8330cb572f567621c91dd613603c85e0466f7dd [file] [log] [blame]
// Copyright 2015 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.h"
#include <algorithm>
#include <limits>
#include <vector>
#include "base/compiler_specific.h"
#include "base/debug/trace_event.h"
#include "base/hash_tables.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/string_split.h"
#include "base/string_util.h"
#include "cobalt/base/polymorphic_downcast.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/can_play_type_handler.h"
#include "starboard/double.h"
namespace cobalt {
namespace dom {
using ::media::WebMediaPlayer;
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::vector<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 (strncasecmp(tokens[i].c_str(), kCodecs, strlen(kCodecs))) {
continue;
}
std::string codec_string = tokens[i].substr(strlen("codecs="));
TrimString(codec_string, " \"", &codec_string);
// SplitString will also trim the results.
::base::SplitString(codec_string, ',', codecs);
break;
}
return !codecs->empty();
}
} // namespace
MediaSource::MediaSource()
: ready_state_(kMediaSourceReadyStateClosed),
player_(NULL),
ALLOW_THIS_IN_INITIALIZER_LIST(event_queue_(this)),
source_buffers_(new SourceBufferList(&event_queue_)) {}
scoped_refptr<SourceBufferList> MediaSource::source_buffers() const {
return source_buffers_;
}
scoped_refptr<SourceBufferList> MediaSource::active_source_buffers() const {
// All source buffers are 'active' as we don't support buffer selection.
return source_buffers_;
}
double MediaSource::duration(script::ExceptionState* exception_state) const {
UNREFERENCED_PARAMETER(exception_state);
if (ready_state_ == kMediaSourceReadyStateClosed) {
return std::numeric_limits<float>::quiet_NaN();
}
DCHECK(player_);
return player_->SourceGetDuration();
}
void MediaSource::set_duration(double duration,
script::ExceptionState* exception_state) {
if (duration < 0.0 || SbDoubleIsNan(duration)) {
DOMException::Raise(DOMException::kInvalidAccessErr, exception_state);
return;
}
if (ready_state_ != kMediaSourceReadyStateOpen) {
DOMException::Raise(DOMException::kInvalidAccessErr, exception_state);
return;
}
DCHECK(player_);
player_->SourceSetDuration(duration);
}
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;
}
std::string mime;
std::vector<std::string> codecs;
if (!ParseContentType(type, &mime, &codecs)) {
DOMException::Raise(DOMException::kNotSupportedErr, exception_state);
// Return value should be ignored.
return NULL;
}
if (!player_ || ready_state_ != kMediaSourceReadyStateOpen) {
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
// Return value should be ignored.
return NULL;
}
// 5. Create a new SourceBuffer object and associated resources.
std::string id = source_buffers_->GenerateUniqueId();
DCHECK(!id.empty());
scoped_refptr<SourceBuffer> source_buffer = new SourceBuffer(this, id);
switch (player_->SourceAddId(source_buffer->id(), mime, codecs)) {
case WebMediaPlayer::kAddIdStatusOk:
source_buffers_->Add(source_buffer);
return source_buffer;
case WebMediaPlayer::kAddIdStatusNotSupported:
DOMException::Raise(DOMException::kNotSupportedErr, exception_state);
// Return value should be ignored.
return NULL;
case WebMediaPlayer::kAddIdStatusReachedIdLimit:
DOMException::Raise(DOMException::kQuotaExceededErr, exception_state);
// Return value should be ignored.
return NULL;
}
NOTREACHED();
return NULL;
}
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 (!player_ || source_buffers_->length() == 0) {
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
return;
}
if (!source_buffers_->Remove(source_buffer)) {
DOMException::Raise(DOMException::kNotFoundErr, exception_state);
return;
}
player_->SourceRemoveId(source_buffer->id());
}
MediaSourceReadyState MediaSource::ready_state() const { return ready_state_; }
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 (!player_ || ready_state_ != kMediaSourceReadyStateOpen) {
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
return;
}
WebMediaPlayer::EndOfStreamStatus eos_status =
WebMediaPlayer::kEndOfStreamStatusNoError;
if (error == kMediaSourceEndOfStreamErrorNoError) {
eos_status = WebMediaPlayer::kEndOfStreamStatusNoError;
} else if (error == kMediaSourceEndOfStreamErrorNetwork) {
eos_status = WebMediaPlayer::kEndOfStreamStatusNetworkError;
} else if (error == kMediaSourceEndOfStreamErrorDecode) {
eos_status = WebMediaPlayer::kEndOfStreamStatusDecodeError;
} else {
DOMException::Raise(DOMException::kInvalidAccessErr, exception_state);
return;
}
SetReadyState(kMediaSourceReadyStateEnded);
player_->SourceEndOfStream(eos_status);
}
// static
bool MediaSource::IsTypeSupported(script::EnvironmentSettings* settings,
const std::string& type) {
DOMSettings* dom_settings =
base::polymorphic_downcast<DOMSettings*>(settings);
DCHECK(dom_settings);
media::CanPlayTypeHandler* handler = dom_settings->can_play_type_handler();
DCHECK(handler);
std::string result = handler->CanPlayType(type, "");
DLOG(INFO) << "MediaSource::IsTypeSupported(" << type << ") -> " << result;
return result == "probably";
}
void MediaSource::SetPlayer(WebMediaPlayer* player) {
// It is possible to reuse a MediaSource object but unlikely. DCHECK it until
// it is used in this way.
DCHECK(!player_);
player_ = player;
}
void MediaSource::ScheduleEvent(base::Token event_name) {
event_queue_.Enqueue(new Event(event_name));
}
scoped_refptr<TimeRanges> MediaSource::GetBuffered(
const SourceBuffer* source_buffer,
script::ExceptionState* exception_state) {
if (!player_ || ready_state_ == kMediaSourceReadyStateClosed) {
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
// Return value should be ignored.
return NULL;
}
::media::Ranges<base::TimeDelta> media_time_ranges =
player_->SourceBuffered(source_buffer->id());
scoped_refptr<TimeRanges> dom_time_ranges = new TimeRanges;
for (int index = 0; index < static_cast<int>(media_time_ranges.size());
++index) {
dom_time_ranges->Add(media_time_ranges.start(index).InSecondsF(),
media_time_ranges.end(index).InSecondsF());
}
return dom_time_ranges;
}
bool MediaSource::SetTimestampOffset(const SourceBuffer* source_buffer,
double timestamp_offset,
script::ExceptionState* exception_state) {
if (!player_ || ready_state_ == kMediaSourceReadyStateClosed) {
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
// Return value should be ignored.
return false;
}
if (!player_->SourceSetTimestampOffset(source_buffer->id(),
timestamp_offset)) {
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
// Return value should be ignored.
return false;
}
return true;
}
void MediaSource::Append(const SourceBuffer* source_buffer, const uint8* buffer,
int size, script::ExceptionState* exception_state) {
if (!buffer) {
DOMException::Raise(DOMException::kInvalidAccessErr, exception_state);
return;
}
if (!player_ || ready_state_ == kMediaSourceReadyStateClosed) {
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
return;
}
if (ready_state_ == kMediaSourceReadyStateEnded) {
SetReadyState(kMediaSourceReadyStateOpen);
}
TRACE_EVENT1("media_stack", "MediaSource::Append()", "size", size);
// If size is greater than kMaxAppendSize, we will append the data in multiple
// small chunks with size less than or equal to kMaxAppendSize. This can
// avoid memory allocation spike as ChunkDemuxer may try to allocator memory
// in size around 'append_size * 2'.
const int kMaxAppendSize = 128 * 1024;
int offset = 0;
while (offset < size) {
int chunk_size = std::min(size - offset, kMaxAppendSize);
if (!player_->SourceAppend(source_buffer->id(), buffer + offset,
static_cast<unsigned int>(chunk_size))) {
DOMException::Raise(DOMException::kSyntaxErr, exception_state);
return;
}
offset += chunk_size;
}
}
void MediaSource::Abort(const SourceBuffer* source_buffer,
script::ExceptionState* exception_state) {
if (!player_ || ready_state_ != kMediaSourceReadyStateOpen) {
DOMException::Raise(DOMException::kInvalidStateErr, exception_state);
return;
}
bool aborted = player_->SourceAbort(source_buffer->id());
DCHECK(aborted);
}
void MediaSource::SetReadyState(MediaSourceReadyState ready_state) {
if (ready_state_ == ready_state) {
return;
}
MediaSourceReadyState old_state = ready_state_;
ready_state_ = ready_state;
if (ready_state_ == kMediaSourceReadyStateClosed) {
source_buffers_->Clear();
player_ = NULL;
ScheduleEvent(base::Tokens::sourceclose());
return;
}
if (old_state == kMediaSourceReadyStateOpen &&
ready_state_ == kMediaSourceReadyStateEnded) {
ScheduleEvent(base::Tokens::sourceended());
return;
}
if (ready_state_ == kMediaSourceReadyStateOpen) {
ScheduleEvent(base::Tokens::sourceopen());
}
}
} // namespace dom
} // namespace cobalt