ヘッドレス統合について
リソース競合の問題
メディア再生とUIレンダリングを同じJavaScriptスレッドで実行すると、互いに動作をブロックする状況が発生することがあります。このようなシナリオは主に、メディア再生によってUIの応答性が妨げられる場合と、UI操作によってスムーズなメディア再生が妨げられる場合の2つに分けられます。 MPEG DASHやHLSなどのストリーミング標準に対応する一般的なメディアプレーヤーの実装では、CPUとネットワークから大量のシステムリソースが消費される可能性があります。特に、再生とライブストリーミングのプロセスでは、CPU負荷の高いマニフェスト解析が必要になります。プレーヤーがUIと同じスレッドで動作していると、両方のコンポーネントがリソースを奪い合い、再生開始の遅延、ビデオ再生の途切れ、UI要素の応答停止、全体的なユーザーエクスペリエンスの低下が発生します。 ヘッドレス実装では、UIから切り離されたJSスレッドでプレーヤーを実行し、プレーヤー用のサービスコンポーネントを作成することでこれらの問題を解決します。UIコントロールは、サービスコンポーネントを操作してメディアの再生を制御できます。このシナリオでは、対話型コンポーネント内のプレーヤーUIが「クライアント」で、サービスコンポーネントを実行しているプレーヤーが「サーバー」となります。 ヘッドレス統合の利点には、再生の開始にかかる時間の短縮、UIの応答性の向上、全体的な再生パフォーマンスの向上などがあります。 アプリにヘッドレス再生を実装するには、次の3つのタスクを実行する必要があります。- プレーヤーサーバーをアプリ内のサービスコンポーネントとして作成します。
- プレーヤーのコードを変更して、プレーヤーを直接制御するのではなくサービスを操作するようにします。
- アプリからサービスを開始します。
詳細な実装手順については、後述のヘッドレスサービスの実装を参照してください。
ヘッドレスサービスの実装
ヘッドレスサービスを実装するには、次の手順を実行します。- アプリのマニフェストにコンポーネントと機能を追加します。
- service.jsファイルを作成します。
- ヘッドレスサービスファイルを作成します。
-
プレーヤーのコードを更新します。
- クライアントを初期化します。
- ビデオの読み込みを処理します。
- 再生コントロールを処理します。
- 追加の依存関係をインストールします。
アプリマニフェストへのコンポーネントの追加
Vegaアプリパッケージでは、パッケージのルートにmanifest.tomlという構成ファイルが含まれている必要があります。このファイルは、パッケージに関する重要な情報を記述します。 アプリにヘッドレス再生を実装するには、アプリのマニフェストでcomponents、processes、wants、offersの各セクションを更新する必要があります。以下の例は、セクションごとの固有の更新方法を示しています。を実際のアプリのパッケージ名に置き換えてください。
[[components.interactive]]
id = "<アプリパッケージ名>.main"
runtime-module = "/com.amazon.kepler.keplerscript.runtime.loader_2@IKeplerScript_2_0"
launch-type = "singleton"
categories = ["com.amazon.category.main"]
[[components.service]]
id = "<アプリパッケージ名>.service"
runtime-module = "/com.amazon.kepler.keplerscript.runtime.loader_2@IKeplerScript_2_0"
launch-type = "singleton"
.
.
.
[processes]
# プレーヤーUIとヘッドレスJSプレーヤーコンポーネントを同じプロセスグループに指定します。
[[processes.group]]
component-ids = ["<アプリパッケージ名>.main", "<アプリパッケージ名>.service"]
.
.
.
[wants]
[[wants.service]]
id = "<アプリパッケージ名>.service"
.
.
.
[offers]
[[offers.service]]
id = "<アプリパッケージ名>.service"
service.jsファイルの作成
プロジェクトのルートにservice.jsファイルを作成します。service.jsファイルを使用することで、ヘッドレスJSサービスのエントリポイント(onStartServiceとonStopService)を登録できます。<アプリパッケージ名>は上記のmanifest.tomlファイルで指定したアプリのパッケージ名に置き換えて、エントリポイントと終了ポイントを登録してください。import {HeadlessEntryPointRegistry} from '@amazon-devices/headless-task-manager';
import {onStartService, onStopService} from './src/PlayerService';
HeadlessEntryPointRegistry.registerHeadlessEntryPoint(
'<アプリパッケージ名>.service::onStartService',
() => onStartService,
);
HeadlessEntryPointRegistry.registerHeadlessEntryPoint(
'<アプリパッケージ名>.service::onStopService',
() => onStopService,
);
ヘッドレスサービスファイルの作成
./src/PlayerService.tsというヘルパーファイルを作成して、プレーヤーの初期化とコントロールを処理します。
サービスファイルのコード
サービスファイルのコード
import { AudioTrack, HTMLMediaElement, TextTrack, VideoPlayer, VideoTrack } from '@amazon-devices/react-native-w3cmedia/dist/headless';
import {
Event,
EventListener,
TrackEvent,
} from '@amazon-devices/react-native-w3cmedia/dist/headless';
import { ShakaPlayer } from './shakaplayer/ShakaPlayer';
import {
IHttpHeader,
IPlayerSessionId,
IPlayerSessionLoadParams,
IPlayerSessionMediaInfo,
IPlayerSessionPosition,
IPlayerSessionState,
IPlayerSessionStatus,
ITimeRange,
ITrackType,
IViewHandle,
} from '@amazon-devices/kepler-player-server';
import {
IPlayerServer,
IPlayerServerHandler,
IPlayerServerFactory,
PlayerServerFactory,
} from '@amazon-devices/kepler-player-server';
class PlayerServerHandler implements IPlayerServerHandler {
// 必要に応じてその他の操作を実行します。
constructor(playerService: PlayerService) {
console.log('[PlayerService] VegaPlayerServerHandler ctor');
this.#playerService = playerService;
}
handleLoad(mediaInfo: IPlayerSessionMediaInfo, loadParams?: IPlayerSessionLoadParams, sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleLoadが呼び出されました。引数:', {
mediaInfo,
loadParams,
sessionId,
});
this.#playerService?.onLoad(mediaInfo, loadParams, sessionId);
}
handleUnload(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleUnloadが呼び出されました。引数:', {
sessionId
});
this.#playerService?.onUnload();
}
handleSetVideoView(handle: IViewHandle, sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleSetVideoViewが呼び出されました。引数:', {
handle,
sessionId
});
this.#playerService?.onSurfaceViewCreated(handle.handle);
}
handleClearVideoView(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleClearVideoViewが呼び出されました。sessionId:', sessionId);
this.#playerService?.onSurfaceViewDestroyed();
}
handleSetTextView(handle: IViewHandle, sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleSetVideoViewが呼び出されました。引数:', {
handle,
sessionId
});
this.#playerService?.onCaptionViewCreated(handle.handle);
}
handleClearTextView(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleClearTextViewが呼び出されました。引数:', {
sessionId
});
this.#playerService?.onCaptionViewDestroyed(handle.handle);
}
handlePlay(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handlePlayが呼び出されました。sessionId:', sessionId);
this.#playerService?.handlePlay();
}
handlePause(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handlePauseが呼び出されました。sessionId:', sessionId);
this.#playerService?.handlePause();
}
handleSeek(position: number, isRelative?: boolean, sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleSeekが呼び出されました。引数:', {
position,
sessionId
});
this.#playerService?.handleSeek(position);
}
handleSetMute(isMuted: boolean, sessionId?: IPlayerSessionId): void {
console.debug('[PlayerService] handleSetMuteが呼び出されました。引数:', {
sessionId
});
this.#playerService?.handleSetMute(isMuted);
}
handleSetPlaybackRate(playbackRate: number, sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleSetPlaybackRateが呼び出されました。引数:', {
sessionId
});
}
handleSetVolume(volume: number, sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleSetVolumeが呼び出されました。引数:', {
volume,
sessionId
});
this.#playerService?.handleSetVolume(volume);
}
handleSetActiveTrack(trackType: ITrackType, trackId: string, sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleSetActiveTrackが呼び出されました。引数:', {
trackType,
trackId,
sessionId
});
this.#playerService.handleSetActiveTrack(String(trackType), trackId);
}
handleGetCurrentPosition(sessionId?: IPlayerSessionId): number {
console.log('[PlayerService] handleGetCurrentPositionが呼び出されました。引数:', {
sessionId
});
return this.#playerService.getCurrentPlaybackPosition().position;
}
handleMessage(message: any, sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleMessageが呼び出されました。引数:', {
sessionId
});
}
handleStartBufferedRangesUpdates(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleStartBufferedRangesUpdatesが呼び出されました。引数:', {
sessionId
});
}
handleStopBufferedRangesUpdates(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleStopBufferedRangesUpdatesが呼び出されました。引数:', {
sessionId
});
}
handleStartStatusUpdates(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleStartStatusUpdatesが呼び出されました。引数:', {
sessionId
});
}
handleStopStatusUpdates(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleStopStatusUpdatesが呼び出されました。引数:', {
sessionId
});
}
handleStartTrackUpdates(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleStartTrackUpdatesが呼び出されました。引数:', {
sessionId
});
}
handleStopTrackUpdates(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleStopTrackUpdatesが呼び出されました。引数:', {
sessionId
});
}
handleStartMessageUpdates(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleStartMessageUpdatesが呼び出されました。引数:', {
sessionId
});
}
handleStopMessageUpdates(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleStopMessageUpdatesが呼び出されました。引数:', {
sessionId
});
}
handleStartErrorUpdates(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleStartErrorUpdatesが呼び出されました。引数:', {
sessionId
});
}
handleStopErrorUpdates(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleStopErrorUpdatesが呼び出されました。引数:', {
sessionId
});
}
}
class PlayerService {
// 必要に応じてその他の操作を実行します。
// 必要に応じてその他の操作を実行します。
// 必要に応じてその他の操作を実行します。
// 必要に応じてその他の操作を実行します。
// 必要に応じてその他の操作を実行します。
#serviceComponentId: string = "com.yourcompany.kepler.headlessjsmediaplayer.service";
#activeSurfaceHandle: string | undefined;
#activeCaptionHandle: string | undefined;
#activeSessionId: IPlayerSessionId | undefined;
#hasError: boolean = false;
#AUTOPLAY: boolean = true;
#initializeVideoPlayer = async (): Promise<void> => {
console.log('[PlayerService] initializeVideoPlayer');
this.#videoPlayer = new VideoPlayer();
// @ts-ignore
global.gmedia = this.#videoPlayer;
await this.#videoPlayer.initialize();
this.#setUpEventListeners();
this.#videoPlayer.autoplay = this.#AUTOPLAY;
this.#initialiseMsePlayer();
};
#initialiseMsePlayer = () => {
console.log('[PlayerService] initialiseMsePlayer');
if (this.#videoPlayer !== undefined) {
this.#msePlayer = new ShakaPlayer(this.#videoPlayer as HTMLMediaElement, {
secure: false, // 再生をセキュアモードまたは非セキュアモードで実行
abrEnabled: true, // アダプティブビットレート(ABR)の切り替えを有効化
abrMaxWidth: 3840, // ABRで許容される最大幅
abrMaxHeight: 2160, // ABRで許容される最大高さ
startPosition: 0
});
}
};
#setUpEventListeners = (): void => {
console.log('[PlayerService] setUpEventListeners');
this.#videoPlayer?.addEventListener('play', this.#onPlay);
this.#videoPlayer?.addEventListener('pause', this.#onPause);
this.#videoPlayer?.addEventListener('seeked', this.#onSeeked);
this.#videoPlayer?.addEventListener('ended', this.#onEnded);
this.#videoPlayer?.addEventListener('error', this.#onError);
this.#videoPlayer?.audioTracks.addEventListener('addtrack', this.#onAudioTrackAdded);
this.#videoPlayer?.videoTracks.addEventListener('addtrack', this.#onVideoTrackAdded);
this.#videoPlayer?.textTracks.addEventListener('addtrack', this.#onTextTrackAdded);
this.#videoPlayer?.audioTracks.addEventListener('removetrack', this.#onAudioTrackRemoved);
this.#videoPlayer?.videoTracks.addEventListener('removetrack', this.#onVideoTrackRemoved);
this.#videoPlayer?.textTracks.addEventListener('removetrack', this.#onTextTrackRemoved);
(this.#videoPlayer as HTMLMediaElement)?.addEventListener('waiting', this.#updateBufferedRanges);
(this.#videoPlayer as HTMLMediaElement)?.addEventListener('canplay', this.#updateBufferedRanges);
};
#removeEventListeners = (): void => {
console.log('[PlayerService] removeEventListeners');
this.#videoPlayer?.removeEventListener('play', this.#onPlay);
this.#videoPlayer?.removeEventListener('pause', this.#onPause);
this.#videoPlayer?.removeEventListener('seeked', this.#onSeeked);
this.#videoPlayer?.removeEventListener('ended', this.#onEnded);
this.#videoPlayer?.removeEventListener('error', this.#onError);
this.#videoPlayer?.audioTracks.removeEventListener('addtrack', this.#onAudioTrackAdded);
this.#videoPlayer?.videoTracks.removeEventListener('addtrack', this.#onVideoTrackAdded);
this.#videoPlayer?.textTracks.removeEventListener('addtrack', this.#onTextTrackAdded);
this.#videoPlayer?.audioTracks.removeEventListener('removetrack', this.#onAudioTrackRemoved);
this.#videoPlayer?.videoTracks.removeEventListener('removetrack', this.#onVideoTrackRemoved);
this.#videoPlayer?.textTracks.removeEventListener('removetrack', this.#onTextTrackRemoved);
(this.#videoPlayer as HTMLMediaElement)?.removeEventListener('waiting', this.#updateBufferedRanges);
(this.#videoPlayer as HTMLMediaElement)?.removeEventListener('canplay', this.#updateBufferedRanges);
};
#onPlay: EventListener = (event?: Event) => {
console.log(`[PlayerService] onPlay ${event?.type}`);
this.updateStatus(this.#activeSessionId);
}
#onPause: EventListener = (event?: Event) => {
console.log('[PlayerService] onPause');
this.updateStatus(this.#activeSessionId);
}
#onSeeked: EventListener = (event?: Event) => {
console.log('[PlayerService] onSeeked');
}
#onEnded: EventListener = (event?: Event) => {
console.log('[PlayerService] onEnded');
this.updateStatus(this.#activeSessionId);
this.onUnload();
}
#onError: EventListener = (event?: Event) => {
console.log(`[PlayerService] onErrorが呼び出されました。コード:{}、メッセージ:{}`,
this.#videoPlayer?.error.code, this.#videoPlayer?.error.message);
this.#hasError = true;
};
#onAudioTrackAdded: EventListener = (event?: Event) => {
if (event instanceof TrackEvent) {
let track = (event as TrackEvent).track;
if (track !== undefined) {
this.#playerServer?.addTrack({
id: track.id,
type: "AUDIO",
kind: track.kind,
label: track.label,
language: track.language,
enabled: (track as AudioTrack).enabled
}, this.#activeSessionId);
// 必要に応じてその他の操作を実行します。
} else {
console.error("オーディオトラックが未定義です。");
}
}
}
#onVideoTrackAdded: EventListener = (event?: Event) => {
if (event instanceof TrackEvent) {
let track = (event as TrackEvent).track;
if (track !== undefined) {
this.#playerServer?.addTrack({
id: track.id,
type: "VIDEO",
kind: track.kind,
label: track.label,
language: track.language,
enabled: (track as VideoTrack).selected
}, this.#activeSessionId);
// 必要に応じてその他の操作を実行します。
} else {
console.error("ビデオトラックが未定義です。");
}
}
}
#onTextTrackAdded: EventListener = (event?: Event) => {
if (event instanceof TrackEvent) {
let track = (event as TrackEvent).track;
if (track !== undefined) {
this.#playerServer?.addTrack({
id: track.id,
type: "TEXT",
kind: track.kind,
label: track.label,
language: track.language,
mode: (track as TextTrack).mode
}, this.#activeSessionId);
// 必要に応じてその他の操作を実行します。
} else {
console.error("テキストトラックが未定義です。");
}
}
}
#onAudioTrackRemoved: EventListener = (event?: Event) => {
if (event instanceof TrackEvent) {
let track = (event as TrackEvent).track;
if (track !== undefined) {
this.#playerServer?.removeTrack({
id: track.id,
type: "AUDIO",
kind: track.kind,
label: track.label,
language: track.language,
enabled: (track as AudioTrack).enabled
}, this.#activeSessionId);
// 必要に応じてその他の操作を実行します。
} else {
console.error("オーディオトラックが未定義です。");
}
}
}
#onVideoTrackRemoved: EventListener = (event?: Event) => {
if (event instanceof TrackEvent) {
let track = (event as TrackEvent).track;
if (track !== undefined) {
this.#playerServer?.removeTrack({
id: track.id,
type: "VIDEO",
kind: track.kind,
label: track.label,
language: track.language,
enabled: (track as VideoTrack).selected
}, this.#activeSessionId);
// 必要に応じてその他の操作を実行します。
} else {
console.error("ビデオトラックが未定義です。");
}
}
}
#onTextTrackRemoved: EventListener = (event?: Event) => {
if (event instanceof TrackEvent) {
let track = (event as TrackEvent).track;
if (track !== undefined) {
this.#playerServer?.removeTrack({
id: track.id,
type: "TEXT",
kind: track.kind,
label: track.label,
language: track.language,
mode: (track as TextTrack).mode
}, this.#activeSessionId);
// 必要に応じてその他の操作を実行します。
} else {
console.error("テキストトラックが未定義です。");
}
}
}
#updateBufferedRanges: EventListener = (event?: Event) => {
const result: Array<ITimeRange> = [];
const bufferedTimeRanges = this.#videoPlayer?.buffered;
if (bufferedTimeRanges === undefined) {
return;
}
for (let i = 0; i < bufferedTimeRanges.length; ++i) {
result.push({
start: bufferedTimeRanges.start(i),
end: bufferedTimeRanges.end(i)
});
}
this.#playerServer?.updateBufferedRanges(result, this.#activeSessionId);
}
#getPlaybackState = () : IPlayerSessionState => {
if (!this.#videoPlayer) return IPlayerSessionState.ENDED;
if (this.#hasError) return IPlayerSessionState.ERROR;
if (this.#videoPlayer.ended) return IPlayerSessionState.ENDED;
if (this.#videoPlayer.seeking) return IPlayerSessionState.SEEKING;
if (this.#videoPlayer.paused) {
return IPlayerSessionState.PAUSED;
}
return IPlayerSessionState.PLAYING;
}
updateStatus = (sessionId?: IPlayerSessionId) => {
console.debug(`[PlayerService] updateStatusが呼び出されました。sessionId:${sessionId !== undefined ? sessionId?.id: "undefined"}`);
const playerSessionStatus: IPlayerSessionStatus = {
sessionId: sessionId,
playbackState: this.#getPlaybackState(),
playbackRate: this.#videoPlayer?.playbackRate || 1,
isMuted: this.#videoPlayer?.muted || false,
volume: this.#videoPlayer?.volume || 0,
seekable: (this.#videoPlayer?.seekable.length || 0) > 0,
duration: this.#videoPlayer?.duration
}
this.#playerServer?.updateStatus([playerSessionStatus]);
}
getCurrentPlaybackPosition = () : IPlayerSessionPosition => {
return {
sessionId: this.#activeSessionId,
position: this.#videoPlayer?.currentTime ?? 0 as number,
} as IPlayerSessionPosition;
}
onSurfaceViewCreated = (surfaceHandle: string): void => {
console.log('[PlayerService] onSurfaceViewCreatedが呼び出されました。surfaceHandle:', surfaceHandle);
this.#activeSurfaceHandle = surfaceHandle;
this.#videoPlayer?.setSurfaceHandle(surfaceHandle);
};
onSurfaceViewDestroyed = (): void => {
console.log('[PlayerService] onSurfaceViewDestroyedが呼び出されました');
if (this.#activeSurfaceHandle !== undefined) {
this.#videoPlayer?.clearSurfaceHandle(this.#activeSurfaceHandle);
}
}
onCaptionViewCreated = (surfaceHandle: string): void => {
console.log('[PlayerService] onCaptionViewCreatedが呼び出されました。surfaceHandle:', surfaceHandle);
this.#activeCaptionHandle = surfaceHandle;
this.#videoPlayer?.setCaptionViewHandle(surfaceHandle);
};
onCaptionViewDestroyed = (): void => {
console.log('[PlayerService] onCaptionViewDestroyedが呼び出されました');
if (this.#activeCaptionHandle !== undefined) {
this.#videoPlayer?.clearCaptionViewHandle(this.#activeCaptionHandle);
}
}
#findValueByKey = (httpHeaders: Array<IHttpHeader> | undefined, key: string) : string | undefined => {
if (httpHeaders !== undefined) {
for (let i : number = 0; i < httpHeaders.length; ++i) {
if (httpHeaders[i].name === key) {
return httpHeaders[i].value;
}
}
}
return undefined;
}
onLoad = async (urlInfo: IPlayerSessionMediaInfo,
loadParams?: IPlayerSessionLoadParams,
sessionId? : IPlayerSessionId): Promise<void> => {
console.log(`[PlayerService] onLoadが呼び出されました。urlInfo:{}`, JSON.stringify(urlInfo));
if (loadParams !== undefined) {
console.log(`[PlayerService] onLoadのloadParams:{}`, JSON.stringify(loadParams));
}
this.#hasError = false;
const content = {
"uri" : urlInfo.mediaUrl.url,
"container": this.#findValueByKey(urlInfo.mediaUrl.httpHeaders, 'container') || 'FMP4'
};
this.onUnload();
this.#activeSessionId = sessionId;
await this.#initializeVideoPlayer();
this.#msePlayer?.load(content, this.#AUTOPLAY);
}
onUnload = () => {
console.log('[PlayerService] onUnload');
this.#removeEventListeners();
this.#msePlayer?.unload();
this.#msePlayer = undefined;
this.#videoPlayer?.deinitialize();
this.#videoPlayer = undefined;
this.#hasError = false;
// @ts-ignore
global.gmedia = null;
};
handlePlay = () => {
console.log('[PlayerService] handlePlay');
if (this.#videoPlayer) {
this.#videoPlayer.play();
} else {
console.log('[PlayerService] handlePlay this.#videoPlayer is undefined');
}
};
handlePause = () => {
console.log('[PlayerService] handlePause');
if (this.#videoPlayer) {
this.#videoPlayer.pause();
} else {
console.log('[PlayerService] handlePause this.#videoPlayer is undefined');
}
};
handleSeek = (seekTimeSeconds: number) => {
console.log(`[PlayerService] handleSeek with seekTimeSeconds: ${seekTimeSeconds}`);
if (this.#videoPlayer) {
const currentTime = this.#videoPlayer.currentTime;
this.#videoPlayer.currentTime = currentTime + seekTimeSeconds;
} else {
console.log('[PlayerService] handleSeek this.#videoPlayer is undefined');
}
}
handleSetMute = (isMuted: boolean) => {
console.debug('[PlayerService] handleSetMute');
if (this.#videoPlayer) {
this.#videoPlayer.muted = isMuted;
this.updateStatus(this.#activeSessionId);
} else {
console.log('[PlayerService] handleSetMute this.#videoPlayer is undefined');
}
};
handleSetVolume = (volume: number) => {
console.debug('[PlayerService] handleSetVolume');
if (this.#videoPlayer) {
this.#videoPlayer.volume = volume;
this.updateStatus(this.#activeSessionId);
}
}
handleSetActiveTrack = (trackType: string, trackId: string) => {
console.debug(`[PlayerService] handleSetActiveTrack ${trackType} ${trackId}`);
if (trackType === 'AUDIO') {
if (this.#msePlayer) {
const audioTrackList = this.#msePlayer.getAudioLanguages();
if (audioTrackList !== undefined && audioTrackList.length > 0) {
let trackLang: string = audioTrackList[parseInt(trackId)];
this.#msePlayer.selectAudioLanguage(trackLang);
} else {
console.error('[PlayerService] handleSetActiveTrack - audio track list is empty.');
}
} else {
console.error('[PlayerService]: handleSetActiveTrack msePlayer not initialised');
}
} else {
console.debug('[PlayerService] handleSetActiveTrack - only audio track handling is implemented.');
}
}
async start(): Promise<void> {
console.log('[PlayerService] start');
this.#playerServerFactory = new PlayerServerFactory();
this.#playerServer = this.#playerServerFactory.getOrMakeServer();
this.#playerServerHandler = new PlayerServerHandler(this);
if (this.#playerServerHandler !== undefined) {
console.log('[PlayerService] Calling setHandler');
this.#playerServer?.setHandler(
this.#playerServerHandler,
this.#serviceComponentId);
}
await this.#initializeVideoPlayer();
console.log('[PlayerService] Service started');
}
async stop(): Promise<void> {
this.onUnload();
}
}
const playerService: PlayerService = new PlayerService();
// Lifecycle methods
export async function onStartService(): Promise<void> {
console.log('[PlayerService] Called onStartService()');
// set global.navigator and global.window needed by @amazon-devices/react-native-w3cmedia
// This is a temporary workaround till either @amazon-devices/react-native-w3cmedia or headless runtime
// do this on behalf of the app developer
// @ts-ignore
let navigator = global.navigator;
if (navigator === undefined) {
// @ts-ignore
global.navigator = navigator = {};
}
/**
* Sets up global variables for React Native.
* You can use this module directly, or just require InitializeCore.
*/
// @ts-ignore
if (global.window === undefined) {
// $FlowExpectedError[cannot-write] The global isn't writable anywhere but here, where we define it.
// @ts-ignore
global.window = global;
}
// @ts-ignore
if (global.self === undefined) {
// $FlowExpectedError[cannot-write] The global isn't writable anywhere but here, where we define it.
// @ts-ignore
global.self = global;
}
await playerService.start();
console.log('[PlayerService] Service started');
}
export async function onStopService(): Promise<void> {
console.log('[PlayerService] Called onStopService()');
await playerService.stop();
console.log('[PlayerService] Service stopped');
}
プレーヤーコードの更新
クライアントの初期化
App.tsxファイルに以下のコードを追加して、クライアントを初期化します。クライアントを初期化するコード
クライアントを初期化するコード
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { IPlayerClientFactory, IPlayerClient } from '@amazon-devices/kepler-player-client';
import { IPlayerSessionId } from '@amazon-devices/kepler-player-server';
import AppStack from './components/AppStack'
const App = () => {
const playerClientFactory = useRef<IPlayerClientFactory | undefined>(undefined);
const playerClient = useRef<IPlayerClient | undefined>(undefined);
const playerSessionId = useRef<IPlayerSessionId | undefined>(undefined);
const playerServiceComponentId = "com.yourcompany.kepler.headlessjsmediaplayer.service";
// プレーヤーを初期化する非同期関数
const initializePlayer = async () => {
console.log('[PlayerUI] 開始');
console.log('[PlayerUI] PlayerClientFactoryを作成します');
playerClientFactory.current = new PlayerClientFactory();
console.log('[PlayerUI] Creating PlayerClient');
playerClient.current
= playerClientFactory.current.getOrMakeClient(playerServiceComponentId);
if (!playerClient.current) {
console.log('[PlayerUI] PlayerClientの作成エラー');
throw new Error('VegaPlayerClientの初期化に失敗しました');
}
playerSessionId.current = { id: Math.floor(Math.random() * 1000) };
}
React.useEffect(() => {
// プレーヤーの初期化を呼び出します。
initializePlayer();
}, [])
return (
<Provider store={store}>
<ThemeProvider theme={theme}>
<NavigationContainer>
{/* IPlayerClientを渡します。 */}
<AppStack playerClient={playerClient} />
</NavigationContainer>
</ThemeProvider>
</Provider>
);
};
export default App;
const styles = StyleSheet.create({
container: {
flex: 1
}
});
ビデオの読み込みの処理
次に、App.tsxファイルに以下のコードを追加して、ビデオの読み込みを処理します。ビデオの読み込みを処理するコード
ビデオの読み込みを処理するコード
// keplerPlayerClientを使用して、コンテンツをプレーヤーコンポーネントに読み込みます。
export const VideoTile = ({playerClient}) => {
const positionListener = useRef<IPlayerSessionPositionListener>({
onPositionUpdated: (updatedPosition: Array<IPlayerSessionPosition>) => {
console.debug('[PlayerUI] onPositionUpdated', JSON.stringify(updatedPosition));
const currentSessionId = playerSessionId.current;
console.debug(`[PlayerUI] onPositionUpdated currentSessionId: ${currentSessionId?.id}`);
const updatePositionForSession = updatedPosition.find(
position => position.sessionId?.id === currentSessionId?.id
) ?? null;
setPlaybackPosition(updatePositionForSession?.position + 's');
}
});
const registerPlayerSessionPositionListener = async() => {
if (positionListenerSubscription.current === undefined) {
positionListenerSubscription.current = await playerClient.current?.registerPositionListener(positionListener.current, 1, playerSessionId.current); // 1秒ごとに新しいステータスの更新を受信
console.debug('[PlayerUI] IPlayerSessionPositionListenerが登録されました');
} else {
console.debug('[PlayerUI] IPlayerSessionPositionListenerは既に登録されています');
}
}
const unregisterPlayerSessionPositionListener = () : void => {
if (positionListenerSubscription.current !== undefined) {
positionListenerSubscription.current.unsubscribe();
positionListenerSubscription.current = undefined;
console.debug('[PlayerUI] IPlayerSessionPositionListenerの登録が解除されました');
} else {
console.debug('[PlayerUI] IPlayerSessionPositionListenerは既に登録解除されています');
}
}
// contentオブジェクトの値は次のようになります。
// {
// title: 'ビデオ1',
// secure: 'false', // true: セキュアなビデオバッファーを使用します。false: 非セキュアなビデオバッファーを使用します。
// uri: 'https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8',
// drm_scheme: '', // com.microsoft.playready、com.widevine.alpha
// drm_license_uri: '', // DRMライセンス取得サーバーのURL(コンテンツがDRMで保護されている場合にのみ必要)
// container: 'FMP4',
// }
const handleVideoSelection = async(content) => {
console.log('[PlayerUI] 選択されたビデオ:', content.title);
console.log(`[PlayerUI] IPlayerClientのloadを呼び出します。引数:`, {
url: content.uri,
sessionId: playerSessionId.current,
});
try {
await playerClient.current?.load(
{
mediaUrl: {
url: content.uri,
httpHeaders: [
{
name: 'container',
value: content.container
}
]
}
} as IPlayerSessionMediaInfo,
{ startPosition: 0, autoPlay: true} as IPlayerSessionLoadParams,
playerSessionId.current
);
} catch (error) {
console.error('loadの呼び出し中にエラーが発生しました:', error);
// エラー処理を実行します。
}
if (autoPlay.current) {
registerPlayerSessionPositionListener();
}
await setActiveSurfaceHandle();
await setActiveCaptionsViewHandle();
}
return (
<View>
{/* この例には記述していませんが、多数のビデオタイルがあると想定します。 */}
{/* ... */ }
<YourVideoCard>
<TouchableOpacity
key={index}
style={styles.playlistItem}
onPress={() => handleVideoSelection(item)}
>
<Text style={styles.playlistText}>{item.title}</Text>
</TouchableOpacity>
<YourVideoCard>
</View>
)
}
try {
await playerClient.current?.load(
{
mediaUrl: {
url: content.uri,
httpHeaders: [
{
name: 'container',
value: content.container
}
]
}
} as IPlayerSessionMediaInfo,
{ startPosition: 0, autoPlay: true} as IPlayerSessionLoadParams,
playerSessionId.current
);
} catch (error) {
console.error('loadの呼び出し中にエラーが発生しました:', error);
// エラー処理を実行します。
}
再生コントロールの処理
次に、App.tsxファイルに以下のコードを追加して、再生コントロールを処理します。再生コントロールを処理するコード
再生コントロールを処理するコード
// keplerPlayerClientを使用して、playerUIが配置された場所の再生/一時停止イベントを処理します。
export const Player = ({playerClient}) {
const handlePlay = async (): Promise<void> => {
console.log('[PlayerUI] 再生ボタンが押されました');
registerPlayerSessionPositionListener();
try {
await playerClient.current?.play(playerSessionId.current);
} catch (error) {
console.error('playの呼び出し中にエラーが発生しました:', error);
// エラー処理を実行します。
}
};
const handlePause = async (): Promise<void> => {
console.log('[PlayerUI] 一時停止ボタンが押されました');
unregisterPlayerSessionPositionListener();
try {
await playerClient.current?.pause(playerSessionId.current);
} catch (error) {
console.error('pauseの呼び出し中にエラーが発生しました:', error);
// エラー処理を実行します。
}
};
return (
<YourPlayer>{...}</YourPlayer>
)
Vegaメディアコントロールの統合
Vegaメディアコントロール(VMC)は、Keplerを使用するメディアアプリ開発者が、メディアコントロール用のさまざまな入力モダリティを効率的に統合できるようにする機能を提供します。メディアアプリは通常、再生・一時停止・停止などのアプリ内ユーザーインターフェイスコントロールを備えていますが、ユーザーはリモコンや、Alexaのようなサービスによる音声コマンドなど、追加の操作方法を求めることがよくあります。Vegaメディアコントロールの機能はそれらの多様な入力方法の統合を処理するため、開発者は、スムーズなメディア再生というコアビジネスロジックの開発に専念できます。 ヘッドレスJS再生APIを使用してコンテンツを再生するアプリでは、無料でKMCを統合して、一時停止・再生・シークなどの基本的な再生制御を有効にすることができます。ヘッドレスJS再生APIは、内部でKMCサーバーを作成し、サーバーの状態を公開して、再生・一時停止・シークという基本的なコマンドを処理します。 ヘッドレスJS再生では、基本的な再生に関するデータについてのみKMCサーバーの状態を更新する必要があります。アプリでKMC自体を操作する場合は、ヘッドレスJS再生の統合を使用する必要はありません。コマンドを処理するには、KMCを直接統合する必要があります。この統合は、プレーヤーに対話型コンポーネントが組み込まれているユースケースにのみ適用されます。この場合、アプリでplayer.setMediaControlFocus(componentInstance)を呼び出したり、機能のオーバーライドを実装したりしないでください。
KMCにオプトインするには
- KMCを有効にします。Vega UIコンポーネントライブラリを使用した開発を始めるの説明に従って、manifest.tomlファイルに必要なマニフェストエントリを追加します。
-
package.jsonファイルを更新して、
@amazon-devices/kepler-media-controlsへの依存関係を指定します。 -
対話型コンポーネントのコンポーネントインスタンスを取得します。
import { useComponentInstance , IComponentInstance } from '@amazon-devices/react-native-kepler'; const componentInstance: IComponentInstance = useComponentInstance(); -
IPlayerClient.loadAPIを呼び出す前に、プレーヤーインスタンスにメディアコントロールフォーカスを設定します。playerClient.current.setMediaControlFocus(componentInstance);
playerClientインスタンスでKMCが有効になり、リモコンやAlexa音声制御による一時停止・再生・シークの基本機能がサポートされます。
特定の制御コマンドのオーバーライド
場合によっては、ヘッドレスJS再生APIがデフォルトで実行する特定の制御コマンドの処理を、アプリでオーバーライドすることが必要になります。たとえば、ライブコンテンツの再生中にシークを無効にすることができます。ヘッドレスJS再生APIには、このような動作を実現する方法も用意されています。-
@amazon-devices/kepler-player-clientによって提供されるPlayerClientMediaControlHandlerを継承し、アプリでオーバーライドする必要のある関数をオーバーライドします。import { PlayerClientMediaControlHandler } from "@amazon-devices/kepler-player-client"; import { IMediaSessionId } from '@amazon-devices/kepler-media-controls'; class AppOverrideMediaControlHandler extends PlayerClientMediaControlHandler { async handlePlay(mediaSessionId?: IMediaSessionId) { if(shouldOverride) { // アプリの要件に従ってカスタム処理を実行します。 } else { // デフォルトハンドラーを呼び出します。 super.handlePlay(mediaSessionId); } } }; -
setMediaControlFocusメソッドに、アプリのメディアコントロールハンドラーを第2パラメーターとして渡します。playerClient.current.setMediaControlFocus(componentInstance, new AppOverrideMediaControlHandler());
追加の依存関係のインストール
ヘッドレスサービスを使用するには、次のパッケージをインストールする必要があります。@amazon-devices/kepler-player-server@amazon-devices/kepler-player-client@amazon-devices/react-native-w3cmedia
"dependencies": {
"@amazon-devices/headless-task-manager": "~1.0.0",
"@amazon-devices/kepler-player-server": "~2.0.4",
"@amazon-devices/kepler-player-client": "~2.0.4",
"@amazon-devices/keplerscript-turbomodule-api": "~1.2.1",
"@amazon-devices/react-native-kepler": "~2.0.0",
"@amazon-devices/react-native-w3cmedia": "~2.1.66",
"base-64": "*",
"react": "18.2.0",
"react-native": "0.72.0",
"xmldom": "*"
},
ベストプラクティス
バックグラウンドモード
アプリがバックグラウンドに移行したときは、UIからIPlayerClient::unloadSyncを呼び出して、メディアプレーヤーの読み込みを解除することをお勧めします。同様に、ヘッドレスJSサービスでは、onStopServiceコールバックでメディアプレーヤーの読み込みを解除し、必要なクリーンアップを実行する必要があります。
関連トピック
- Vegaヘッドレスタスクとヘッドレスサービス
- VegaプレーヤークライアントAPIリファレンス
- VegaプレーヤーサーバーAPIリファレンス
- Vegaアプリマニフェスト
- アプリのパフォーマンスに関するベストプラクティス
- VegaメディアコントロールAPIリファレンス

