| /* |
| * 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/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" |
| |
| 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 |
| |
| void MediaSource::Registry::Register( |
| const std::string& blob_url, |
| const scoped_refptr<MediaSource>& media_source) { |
| DCHECK(media_source); |
| DCHECK(media_source_registry_.find(blob_url) == media_source_registry_.end()); |
| media_source_registry_.insert(std::make_pair(blob_url, media_source)); |
| } |
| |
| scoped_refptr<MediaSource> MediaSource::Registry::Retrieve( |
| const std::string& blob_url) { |
| MediaSourceRegistry::iterator iter = media_source_registry_.find(blob_url); |
| if (iter == media_source_registry_.end()) { |
| DLOG(WARNING) << "Cannot find MediaSource object for blob url " << blob_url; |
| return NULL; |
| } |
| |
| return iter->second; |
| } |
| |
| void MediaSource::Registry::Unregister(const std::string& blob_url) { |
| MediaSourceRegistry::iterator iter = media_source_registry_.find(blob_url); |
| if (iter == media_source_registry_.end()) { |
| DLOG(WARNING) << "Cannot find MediaSource object for blob url " << blob_url; |
| return; |
| } |
| |
| media_source_registry_.erase(iter); |
| } |
| |
| MediaSource::MediaSource() |
| : ready_state_(kReadyStateClosed), |
| 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_ == kReadyStateClosed) { |
| 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 || isnan(duration)) { |
| DOMException::Raise(DOMException::kInvalidAccessErr, exception_state); |
| return; |
| } |
| if (ready_state_ != kReadyStateOpen) { |
| 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_ != kReadyStateOpen) { |
| 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()); |
| } |
| |
| std::string MediaSource::ready_state() const { |
| switch (ready_state_) { |
| case kReadyStateClosed: |
| return "closed"; |
| case kReadyStateOpen: |
| return "open"; |
| case kReadyStateEnded: |
| return "ended"; |
| } |
| NOTREACHED() << "ready_state_ (" << ready_state_ << ") is invalid"; |
| return ""; |
| } |
| |
| void MediaSource::EndOfStream(script::ExceptionState* exception_state) { |
| // If there is no error string provided, treat it as empty. |
| EndOfStream("", exception_state); |
| } |
| |
| void MediaSource::EndOfStream(const std::string& error, |
| script::ExceptionState* exception_state) { |
| if (!player_ || ready_state_ != kReadyStateOpen) { |
| DOMException::Raise(DOMException::kInvalidStateErr, exception_state); |
| return; |
| } |
| |
| WebMediaPlayer::EndOfStreamStatus eos_status = |
| WebMediaPlayer::kEndOfStreamStatusNoError; |
| |
| if (error.empty() || error == "null") { |
| eos_status = WebMediaPlayer::kEndOfStreamStatusNoError; |
| } else if (error == "network") { |
| eos_status = WebMediaPlayer::kEndOfStreamStatusNetworkError; |
| } else if (error == "decode") { |
| eos_status = WebMediaPlayer::kEndOfStreamStatusDecodeError; |
| } else { |
| DOMException::Raise(DOMException::kInvalidAccessErr, exception_state); |
| return; |
| } |
| |
| SetReadyState(kReadyStateEnded); |
| 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_ == kReadyStateClosed) { |
| 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_ == kReadyStateClosed) { |
| 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_ == kReadyStateClosed) { |
| DOMException::Raise(DOMException::kInvalidStateErr, exception_state); |
| return; |
| } |
| |
| if (ready_state_ == kReadyStateEnded) { |
| SetReadyState(kReadyStateOpen); |
| } |
| |
| // 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_ != kReadyStateOpen) { |
| DOMException::Raise(DOMException::kInvalidStateErr, exception_state); |
| return; |
| } |
| |
| bool aborted = player_->SourceAbort(source_buffer->id()); |
| DCHECK(aborted); |
| } |
| |
| MediaSource::ReadyState MediaSource::GetReadyState() const { |
| return ready_state_; |
| } |
| |
| void MediaSource::SetReadyState(ReadyState ready_state) { |
| if (ready_state_ == ready_state) { |
| return; |
| } |
| |
| ReadyState old_state = ready_state_; |
| ready_state_ = ready_state; |
| |
| if (ready_state_ == kReadyStateClosed) { |
| source_buffers_->Clear(); |
| player_ = NULL; |
| ScheduleEvent(base::Tokens::sourceclose()); |
| return; |
| } |
| |
| if (old_state == kReadyStateOpen && ready_state_ == kReadyStateEnded) { |
| ScheduleEvent(base::Tokens::sourceended()); |
| return; |
| } |
| |
| if (ready_state_ == kReadyStateOpen) { |
| ScheduleEvent(base::Tokens::sourceopen()); |
| } |
| } |
| |
| } // namespace dom |
| } // namespace cobalt |