Skip to content

Commit

Permalink
[media] Support configurable audio write ahead
Browse files Browse the repository at this point in the history
b/267678497
  • Loading branch information
xiaomings authored and osagie98 committed Nov 1, 2023
1 parent 36a7aa4 commit 54046bd
Show file tree
Hide file tree
Showing 36 changed files with 1,010 additions and 37 deletions.
55 changes: 42 additions & 13 deletions cobalt/demos/content/media-element-demo/src/components/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ export class Player extends Component<Props> {
/** The <video> element. */
private videoEl!: HTMLVideoElement;

/** The video SourceBuffer */
private videoSourceBuffer!: SourceBuffer;

/** The audio SourceBuffer */
private audioSourceBuffer!: SourceBuffer;

/** max(videoSourceBuffer.writeHead - videoEl.currentTime) */
private maxVideoWriteHeadDistance!: number;

/** max(audioSourceBuffer.writeHead - videoEl.currentTime) */
private maxAudioWriteHeadDistance!: number;

/** The element displaying video download buffer info. */
private videoDownloadBufferInfo!: Element;

Expand Down Expand Up @@ -53,6 +65,8 @@ export class Player extends Component<Props> {
super(props);
this.videos = convertToMediaArray(props.video);
this.audios = convertToMediaArray(props.audio);
this.maxVideoWriteHeadDistance = 0;
this.maxAudioWriteHeadDistance = 0;
}

/** @override */
Expand Down Expand Up @@ -93,12 +107,37 @@ export class Player extends Component<Props> {
}

private renderVideoInfo() {
var h5vccAudioConnectors = '';
try {
h5vccAudioConnectors = this.videoEl.h5vccAudioConnectors;
} catch (error) {}
renderComponent(
VideoInfo, {
duration: this.videoEl.duration,
currentTime: this.videoEl.currentTime,
audioConnectors: h5vccAudioConnectors,
},
this.videoInfo);
if (this.videoSourceBuffer) {
this.maxVideoWriteHeadDistance =
Math.max(this.maxVideoWriteHeadDistance,
this.videoSourceBuffer.writeHead - this.videoEl.currentTime);
renderComponent(
SourceBufferInfo,
{name: 'Video', sourceBuffer: this.videoSourceBuffer,
maxWriteHeadDistance: this.maxVideoWriteHeadDistance},
this.videoSourceBufferInfo);
}
if (this.audioSourceBuffer) {
this.maxAudioWriteHeadDistance =
Math.max(this.maxAudioWriteHeadDistance,
this.audioSourceBuffer.writeHead - this.videoEl.currentTime);
renderComponent(
SourceBufferInfo,
{name: 'Audio', sourceBuffer: this.audioSourceBuffer,
maxWriteHeadDistance: this.maxAudioWriteHeadDistance},
this.audioSourceBufferInfo);
}
}

private async play() {
Expand Down Expand Up @@ -149,7 +188,7 @@ export class Player extends Component<Props> {

/**
* Plays all videos as adaptive videos.
* TODO: dynmaically calculate the source buffer MIME.
* TODO: dynamically calculate the source buffer MIME.
*/
private playAdaptiveVideo() {
const ms = new MediaSource();
Expand All @@ -158,12 +197,7 @@ export class Player extends Component<Props> {
if (this.videos.length > 0) {
const videoSourceBuffer =
ms.addSourceBuffer('video/mp4; codecs="avc1.640028"');
videoSourceBuffer.addEventListener('updateend', () => {
renderComponent(
SourceBufferInfo,
{name: 'Video', sourceBuffer: videoSourceBuffer},
this.videoSourceBufferInfo);
});
this.videoSourceBuffer = videoSourceBuffer;
const downloadBuffer = new DownloadBuffer(this.videos);
downloadBuffer.register((reportMap) => {
renderComponent(
Expand All @@ -176,12 +210,7 @@ export class Player extends Component<Props> {
if (this.audios.length > 0) {
const audioSourceBuffer =
ms.addSourceBuffer('audio/mp4; codecs="mp4a.40.2"');
audioSourceBuffer.addEventListener('updateend', () => {
renderComponent(
SourceBufferInfo,
{name: 'Audio', sourceBuffer: audioSourceBuffer},
this.audioSourceBufferInfo);
});
this.audioSourceBuffer = audioSourceBuffer;
const downloadBuffer = new DownloadBuffer(this.audios);
downloadBuffer.register(
(reportMap) => {renderComponent(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
interface Props {
sourceBuffer: SourceBuffer;
name: string;
maxWriteHeadDistance: number;
}

/** A component that displays the source buffer info. */
export function SourceBufferInfo({sourceBuffer, name}: Props) {
return `<div>${name} buffered: ${sourceBuffer.buffered.end(0)} sec</div>`;
export function SourceBufferInfo({sourceBuffer, name, maxWriteHeadDistance}: Props) {
return `<div>${name} buffered: ${sourceBuffer.buffered.end(0)} sec` +
`, writeHead: ${sourceBuffer.writeHead} sec` +
`, maxWriteHeadDistance: ${maxWriteHeadDistance} sec</div>`;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
interface Props {
duration: number;
currentTime: number;
audioConnectors: string;
}

/** A component that displays video info. */
export function VideoInfo({duration, currentTime}: Props) {
export function VideoInfo({duration, currentTime, audioConnectors}: Props) {
if (audioConnectors) {
return `<div>${currentTime} / ${duration} / audioConnectors: ${audioConnectors}</div>`;
}
return `<div>${currentTime} / ${duration}</div>`;
}
38 changes: 35 additions & 3 deletions cobalt/dom/html_media_element.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,28 @@
#include <limits>
#include <memory>
#include <utility>
#include <vector>

#include "base/bind.h"
#include "base/compiler_specific.h"
#include "base/guid.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/message_loop/message_loop.h"
#include "base/strings/string_util.h"
#include "base/trace_event/trace_event.h"
#include "cobalt/base/instance_counter.h"
#include "cobalt/base/tokens.h"
#include "cobalt/cssom/map_to_mesh_function.h"
#include "cobalt/dom/document.h"
#include "cobalt/dom/eme/media_encrypted_event.h"
#include "cobalt/dom/eme/media_encrypted_event_init.h"
#include "cobalt/dom/html_element_context.h"
#include "cobalt/dom/html_video_element.h"
#include "cobalt/dom/media_settings.h"
#include "cobalt/dom/media_source.h"
#include "cobalt/dom/media_source_ready_state.h"
#include "cobalt/extension/audio_write_ahead.h"
#include "cobalt/loader/fetcher_factory.h"
#include "cobalt/media/url_fetcher_data_source.h"
#include "cobalt/media/web_media_player_factory.h"
Expand All @@ -45,9 +50,7 @@
#include "cobalt/web/dom_exception.h"
#include "cobalt/web/event.h"
#include "cobalt/web/web_settings.h"

#include "cobalt/dom/eme/media_encrypted_event.h"
#include "cobalt/dom/eme/media_encrypted_event_init.h"
#include "starboard/system.h"

namespace cobalt {
namespace dom {
Expand Down Expand Up @@ -156,6 +159,17 @@ HTMLMediaElement::HTMLMediaElement(Document* document, base::Token tag_name)
sent_end_event_(false),
request_mode_(loader::kNoCORSMode) {
TRACE_EVENT0("cobalt::dom", "HTMLMediaElement::HTMLMediaElement()");

const CobaltExtensionConfigurableAudioWriteAheadApi* extension_api =
static_cast<const CobaltExtensionConfigurableAudioWriteAheadApi*>(
SbSystemGetExtension(
kCobaltExtensionConfigurableAudioWriteAheadName));
if (extension_api) {
DCHECK_EQ(extension_api->name,
std::string(kCobaltExtensionConfigurableAudioWriteAheadName));
DCHECK_EQ(extension_api->version, 1u);
audio_write_ahead_extension_enabled_ = true;
}
LOG(INFO) << "Create HTMLMediaElement with volume " << volume_
<< ", playback rate " << playback_rate_ << ".";
ON_INSTANCE_CREATED(HTMLMediaElement);
Expand Down Expand Up @@ -641,6 +655,24 @@ void HTMLMediaElement::ScheduleEvent(const scoped_refptr<web::Event>& event) {
event_queue_.Enqueue(event);
}

std::string HTMLMediaElement::h5vcc_audio_connectors(
script::ExceptionState* exception_state) const {
if (audio_write_ahead_extension_enabled_) {
if (!player_) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return std::string();
}

std::vector<std::string> configs = player_->GetAudioConnectors();
return base::JoinString(configs, ";");
}

web::DOMException::Raise(web::DOMException::kNotSupportedErr,
exception_state);
return std::string();
}

void HTMLMediaElement::CreateMediaPlayer() {
TRACE_EVENT0("cobalt::dom", "HTMLMediaElement::CreateMediaPlayer()");
LOG(INFO) << "Create media player.";
Expand Down
8 changes: 8 additions & 0 deletions cobalt/dom/html_media_element.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ class HTMLMediaElement : public HTMLElement,
// function won't modify the target of the |event| passed in.
void ScheduleEvent(const scoped_refptr<web::Event>& event);

// Returns semicolon separated names of audio connectors, like
// "hdmi;bluetooth".
// TODO(b/267678497): The current interface is tentative, to be refined.
std::string h5vcc_audio_connectors(
script::ExceptionState* exception_state) const;

// Set max video capabilities.
void SetMaxVideoCapabilities(const std::string& max_video_capabilities,
script::ExceptionState* exception_state);
Expand Down Expand Up @@ -306,6 +312,8 @@ class HTMLMediaElement : public HTMLElement,

loader::RequestMode request_mode_;

bool audio_write_ahead_extension_enabled_ = false;

DISALLOW_COPY_AND_ASSIGN(HTMLMediaElement);
};

Expand Down
6 changes: 6 additions & 0 deletions cobalt/dom/html_media_element.idl
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,10 @@ interface HTMLMediaElement : HTMLElement {
attribute boolean controls;
[RaisesException] attribute double volume;
attribute boolean muted;

// non standard, semicolon separated names of audio connectors, like
// "hdmi;bluetooth". It raises `NotSupportedError` on apps doesn't support
// this feature, or `InvalidStateError` if there isn't an active playback.
// TODO(b/267678497): The current interface is tentative, to be refined.
[RaisesException] readonly attribute DOMString h5vccAudioConnectors;
};
11 changes: 11 additions & 0 deletions cobalt/dom/source_buffer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,17 @@ void SourceBuffer::set_track_defaults(
track_defaults_ = track_defaults;
}

double SourceBuffer::write_head(script::ExceptionState* exception_state) const {
if (media_source_ == NULL) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
exception_state);
return 0.0;
}

DCHECK(chunk_demuxer_);
return chunk_demuxer_->GetWriteHead(id_).InSecondsF();
}

void SourceBuffer::OnRemovedFromMediaSource() {
if (media_source_ == NULL) {
return;
Expand Down
3 changes: 3 additions & 0 deletions cobalt/dom/source_buffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ class SourceBuffer : public web::EventTarget {

// Custom, not in any spec.
//
// Return the highest presentation timestamp written to SbPlayer.
double write_head(script::ExceptionState* exception_state) const;

void OnRemovedFromMediaSource();
double GetHighestPresentationTimestamp() const;

Expand Down
6 changes: 6 additions & 0 deletions cobalt/dom/source_buffer.idl
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,10 @@ interface SourceBuffer : EventTarget {
// [RaisesException] void abort();
[RaisesException] void remove(double start, unrestricted double end);
[RaisesException] attribute TrackDefaultList trackDefaults;

// Non standard interface (b/267678497).
// Returns the highest presentation timestamp written to SbPlayer, raises
// `InvalidStateError` if the SourceBuffer object has been removed from the
// MediaSource object.
[RaisesException] readonly attribute double writeHead;
};
82 changes: 82 additions & 0 deletions cobalt/extension/audio_write_ahead.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2023 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.

#ifndef COBALT_EXTENSION_AUDIO_WRITE_AHEAD_H_
#define COBALT_EXTENSION_AUDIO_WRITE_AHEAD_H_

#include "starboard/media.h"
#include "starboard/player.h"

#ifdef __cplusplus
extern "C" {
#endif

#define kCobaltExtensionConfigurableAudioWriteAheadName \
"dev.cobalt.extension.ConfigurableAudioWriteAhead"

#define kCobaltExtensionPlayerWriteDurationLocal (kSbTimeSecond / 2)
#define kCobaltExtensionPlayerWriteDurationRemote (kSbTimeSecond * 10)

typedef enum CobaltExtensionMediaAudioConnector {
kCobaltExtensionMediaAudioConnectorUnknown,

kCobaltExtensionMediaAudioConnectorAnalog,
kCobaltExtensionMediaAudioConnectorBluetooth,
kCobaltExtensionMediaAudioConnectorBuiltIn,
kCobaltExtensionMediaAudioConnectorHdmi,
kCobaltExtensionMediaAudioConnectorRemoteWired,
kCobaltExtensionMediaAudioConnectorRemoteWireless,
kCobaltExtensionMediaAudioConnectorRemoteOther,
kCobaltExtensionMediaAudioConnectorSpdif,
kCobaltExtensionMediaAudioConnectorUsb,

} CobaltExtensionMediaAudioConnector;

typedef struct CobaltExtensionMediaAudioConfiguration {
CobaltExtensionMediaAudioConnector connector;

SbTime latency;

SbMediaAudioCodingType coding_type;

int number_of_channels;

} CobaltExtensionMediaAudioConfiguration;

typedef struct CobaltExtensionConfigurableAudioWriteAheadApi {
// Name should be the string
// |kCobaltExtensionConfigurableAudioWriteAheadName|. This helps to validate
// that the extension API is correct.
const char* name;

// This specifies the version of the API that is implemented.
uint32_t version;

// The fields below this point were added in version 1 or later.

bool (*MediaGetAudioConfiguration)(
int output_index,
CobaltExtensionMediaAudioConfiguration* out_configuration);

bool (*PlayerGetAudioConfiguration)(
SbPlayer player, int index,
CobaltExtensionMediaAudioConfiguration* out_audio_configuration);

} CobaltExtensionConfigurableAudioWriteAheadApi;

#ifdef __cplusplus
} // extern "C"
#endif

#endif // COBALT_EXTENSION_AUDIO_WRITE_AHEAD_H_
12 changes: 12 additions & 0 deletions cobalt/media/base/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ class MEDIA_EXPORT Pipeline : public base::RefCountedThreadSafe<Pipeline> {
bool allow_resume_after_suspend, bool allow_batched_sample_write,
MediaLog* media_log, DecodeTargetProvider* decode_target_provider);

static scoped_refptr<Pipeline> Create(
SbPlayerInterface* interface, PipelineWindow window,
const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
const GetDecodeTargetGraphicsContextProviderFunc&
get_decode_target_graphics_context_provider_func,
bool allow_resume_after_suspend, bool allow_batched_sample_write,
SbTime audio_write_duration_local, SbTime audio_write_duration_remote,
MediaLog* media_log, DecodeTargetProvider* decode_target_provider);

virtual ~Pipeline() {}

virtual void Suspend() {}
Expand Down Expand Up @@ -217,6 +226,9 @@ class MEDIA_EXPORT Pipeline : public base::RefCountedThreadSafe<Pipeline> {
// be 0.
virtual void GetNaturalVideoSize(gfx::Size* out_size) const = 0;

// Gets the names of audio connectors used by the audio output.
virtual std::vector<std::string> GetAudioConnectors() const = 0;

// Return true if loading progress has been made since the last time this
// method was called.
virtual bool DidLoadingProgress() const = 0;
Expand Down
Loading

0 comments on commit 54046bd

Please sign in to comment.