关于无头集成
资源争用问题
在两个主要场景中于同一JavaScript线程上运行时,在以下情况下媒体播放和用户界面渲染可能会相互阻塞:当媒体播放阻碍用户界面响应能力时,或者当界面操作阻碍流畅的媒体播放时。 流媒体标准(例如MPEG DASH或HLS)的常见媒体播放器实现可能需要来自CPU和网络的大量系统资源。具体而言,播放和直播过程需要CPU密集型清单解析。当播放器与用户界面在同一线程上运行时,这些组件会争用资源,从而导致播放开始延迟、视频播放不稳定、用户界面元素无响应以及整体用户体验不佳。 无头实现通过在与用户界面分开的JS线程中运行播放器,为播放器创建服务组件来解决这些问题。这使用户界面控件能够与服务组件交互以控制媒体播放。在这种情况下,交互组件中的播放器用户界面是“客户端”,运行服务组件的播放器是“服务器”。 无头集成的好处包括缩短播放开始时间、增强用户界面响应能力和更好的整体播放性能。 要在应用中实现无头播放,您需要执行以下3项任务:- 在应用中将播放器服务器创建为服务组件。
- 更改播放器代码以与服务交互而非直接控制播放器。
- 从应用启动服务。
有关详细的实施步骤,请参阅下面的实现无头服务。
实现无头服务
要实现无头服务,请执行以下步骤:- 向您的应用清单添加组件和功能。
- 创建service.js文件
- 创建无头服务文件。
- 更新您的播放器代码 a. 初始化客户端。 b. 处理视频加载。 c. 处理播放控制。
- 安装其他依赖项。
向应用清单中添加组件
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]
# 确保播放器用户界面和无头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 called with: ', {
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带代码:{}, message: {}`,
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("Undefined text track.");
}
}
}
#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 with 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();
// 生命周期方法
export async function onStartService(): Promise<void> {
console.log('[PlayerService] 已调用onStartService()');
// 通过@amazon-devices/react-native-w3cmedia设置需要的global.navigator
// 在@amazon-devices/react-native-w3cmedia或无头运行时之前,这是一个临时的解决方法
// 代表应用开发者执行此操作
// @ts-ignore
let navigator = global.navigator;
if (navigator === undefined) {
// @ts-ignore
global.navigator = navigator = {};
}
/**
* 为React Native设置全局变量。
* 您可以直接使用此模块,也可以只使用InitializeCore。
*/
// @ts-ignore
if (global.window === undefined) {
// $FlowExpectedError[cannot-write] 除了我们定义它的此处位置,全局变量在任何位置都不可写入。
// @ts-ignore
global.window = global;
}
// @ts-ignore
if (global.self === undefined) {
// $FlowExpectedError[cannot-write] 除了我们定义它的此处位置,全局变量在任何位置都不可写入。
// @ts-ignore
global.self = global;
}
await playerService.start();
console.log('[PlayerService] Service started');
}
export async function onStopService(): Promise<void> {
console.log('[PlayerService] 已调用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] 正在创建PlayerClient');
playerClient.current
= playerClientFactory.current.getOrMakeClient(playerServiceComponentId);
if (!playerClient.current) {
console.log('[PlayerUI] 创建PlayerClient时出错');
throw new Error('Failed to initialize 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已取消注册');
}
}
// 让内容对象中的值为:
// {
// 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加载,使用的是:`, {
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('调用加载时出错:', 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('调用加载时出错:', 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('调用播放时出错:', error);
// 执行错误处理。
}
};
const handlePause = async (): Promise<void> => {
console.log('[PlayerUI] 暂停按钮已按下');
unregisterPlayerSessionPositionListener();
try {
await playerClient.current?.pause(playerSessionId.current);
} catch (error) {
console.error('调用暂停时出错:', error);
// 执行错误处理。
}
};
return (
<YourPlayer>{...}</YourPlayer>
)
集成Vega媒体控制
Vega媒体控制 (VMC) 所提供的功能可让Vega上的媒体应用开发者为媒体控制简化各种输入模式的集成。虽然媒体应用通常具有应用内用户界面控件,例如播放、暂停和停止,但客户通常需要额外的交互方式,例如使用遥控器或通过Alexa等服务使用语音命令。Vega媒体控制功能可处理这些不同的输入方法的集成,让您能够专注于流畅媒体播放的核心业务逻辑。 使用无头JS播放API播放其内容的应用现在可以免费获得KMC集成,用于基本的播放控制,例如暂停、播放和搜索。无头JS播放API在内部创建KMC服务器,发布服务器状态,并处理播放、暂停和搜索等基本命令。 无头JS播放只能更新与基本播放相关的数据的KMC服务器状态。如果应用自行处理KMC,则无需使用无头JS播放集成。它必须直接集成到KMC中才能处理命令。这种集成仅适用于将播放器与交互式组件集成的用例。应用不应调用player.setMediaControlFocus(componentInstance) 或实现覆盖功能。
选择加入KMC
- 启用KMC。按照开始使用Vega媒体控制概述中所述,将必要的清单条目添加到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方法。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": "*"
},
最佳实践
背景模式
当应用进入后台时,建议用户界面调用IPlayerClient::unloadSync来卸载媒体播放器。同样,无头JS服务应卸载媒体播放器,并在其onStopService回调中进行必要的清理。

