/**
* 对外输出接口
*/
export interface VideoPlayerManageInterface {
//初始化视频播放器
init(
list: VideoPlayerManageInitListType[],
options?: VideoPlayerManageInitOptionsType
): Promise<void>;
//播放视频
play(url: string, currentTime?: number): Promise<void>;
//销毁视频
destroyUrl(url: string, currentTag: string): void;
//销毁标签的所有视频
destroyTag(targetTag: string): void;
}
export type VideoPlayerManageInitListType = {
//标签,分组
tag: string;
//视频地址
url: string;
//视频宽度
width: number;
//视频高度
height: number;
//是否循环播放
isLoop: boolean;
//视频播放结束回调(非循环下生效)
endCallback?: () => void;
};
export type VideoPlayerManageInitOptionsType = {
//视频封面地址
poster?: string;
//播放按钮地址
playerStartButtonUrl?: string;
//是否已经点击过播放按钮
hasClicked?: boolean;
//是否是苹果设备检测函数
checkIsAppleFun?: () => boolean;
};
/**
* 鉴于cocos对于视频播放的支持不够完善,在自带的VideoPlayer组件上进行了封装
* 使用VideoPlayerManage进行视频的提前装载,播放,销毁等操作
* 解决视频切换时的黑屏问题、视频默认样式修改、ISO自动播放、播放时未就绪报错的问题
* web环境使用
*/
cc.macro.ENABLE_TRANSPARENT_CANVAS = true;
export default class VideoPlayerManage implements VideoPlayerManageInterface {
private static V: VideoPlayerManageInterface = null;
public static get instance(): VideoPlayerManageInterface {
if (!this.V) {
this.V = new VideoPlayerManage();
}
return this.V;
}
private readonly CLASS_NAME = "cocosVideo";
private poster = ""; //这里是一个视频封面的图片地址
private playerStartButtonUrl = ""; //这里是一个播放按钮的图片地址
private hasClicked = false;
constructor() {
cc.Canvas.instance.node.getComponentInChildren(
cc.Camera
).backgroundColor = new cc.Color(0, 0, 0, 0);
}
private map: Map<
string,
{
url: string;
videoPlayer: cc.VideoPlayer;
tag: string[];
}
> = new Map();
async init(
list: VideoPlayerManageInitListType[],
options?: VideoPlayerManageInitOptionsType
): Promise<void> {
Object.keys(options).forEach((key) => {
this[key] = options[key];
});
await Promise.all(
list.map((listItem) => this.initVideoPlayerCore(listItem))
);
await this.initVideos();
}
async play(url: string, currentTime = 0) {
const videoPlayer = this.map.get(url).videoPlayer;
this.map.forEach((value) => {
value.videoPlayer.node.active = url === value.url;
});
videoPlayer.currentTime = currentTime;
if (videoPlayer.isPlaying()) {
videoPlayer.pause();
}
videoPlayer.node.off("ready-to-play");
videoPlayer.node.on(
"ready-to-play",
() => {
if (!videoPlayer.isPlaying()) {
videoPlayer.play();
}
},
this
);
await this.waitPlayerClick();
if (!videoPlayer.isPlaying()) {
videoPlayer.play();
} else {
videoPlayer.resume();
}
}
destroyUrl(url: string, currentTag: string) {
const item = this.map.get(url);
if (!item) return;
if (item.tag.length > 1) {
item.tag = item.tag.filter((tagItem) => {
return tagItem !== currentTag;
});
return;
}
this.delOneVideo(item);
}
destroyTag(targetTag: string) {
this.map.forEach((item) => {
if (item.tag.includes(targetTag)) {
item.tag = item.tag.filter((tagItem) => {
return tagItem !== targetTag;
});
if (item.tag.length === 0) {
this.delOneVideo(item);
}
}
});
}
private delOneVideo(item) {
item.videoPlayer.node.destroy();
const videoDom = this.getVideoDom(item.url);
if (videoDom) {
videoDom.parentNode.removeChild(videoDom);
}
this.map.delete(item.url);
}
private async initVideos() {
await this.delayOneFrame();
const list = document.getElementsByClassName(this.CLASS_NAME);
for (let i = 0; i < list.length; i++) {
const video: Element = list[i];
video["poster"] = this.poster;
video["autoplay"] = true;
}
}
private async initVideoPlayerCore(listItem) {
const videoMapItem = this.map.get(listItem.url);
if (videoMapItem) {
!videoMapItem.tag.includes(listItem.tag) &&
videoMapItem.tag.push(listItem.tag);
return;
}
const videoPlayer = await this.createVideoPlayerForUrl(listItem);
this.map.set(listItem.url, {
url: listItem.url,
videoPlayer,
tag: [listItem.tag],
});
}
private async createVideoPlayerForUrl(listItem): Promise<cc.VideoPlayer> {
const videoNode: cc.Node = new cc.Node();
videoNode.active = false;
const videoPlayer = videoNode.addComponent(cc.VideoPlayer);
videoPlayer.mute = true;
videoPlayer.resourceType = cc.VideoPlayer.ResourceType.LOCAL;
videoNode.width = listItem.width;
videoNode.height = listItem.height;
videoPlayer.stayOnBottom = true;
cc.Canvas.instance.node.addChild(videoNode);
const asset = await this.loadVideo(listItem.url);
videoPlayer.clip = asset as unknown as string;
this.setLoopAndEndCallBack(
videoPlayer,
listItem.isLoop,
listItem.endCallback
);
return videoPlayer;
}
private loadVideo(url: string) {
return new Promise((resolve, reject) => {
cc.assetManager.loadRemote(
url,
{ ext: ".mp4" },
(err, asset: cc.Asset) => {
if (err) {
reject(err);
} else {
resolve(asset);
}
}
);
});
}
private loadPng(url: string) {
return new Promise((resolve, reject) => {
cc.assetManager.loadRemote(
url,
{ ext: ".png" },
(err, asset: cc.Asset) => {
if (err) {
reject(err);
} else {
resolve(asset);
}
}
);
});
}
private delayOneFrame(): Promise<void> {
return new Promise((resole) => {
cc.Canvas.instance.scheduleOnce(() => {
resole();
});
});
}
private setLoopAndEndCallBack(
videoPlayer: cc.VideoPlayer,
isLoop: boolean,
endCallback: () => void
) {
videoPlayer.node.off("completed");
videoPlayer.node.on(
"completed",
() => {
if (isLoop) {
videoPlayer.currentTime = 0;
videoPlayer.play();
} else {
endCallback && endCallback();
}
},
this
);
}
private async waitPlayerClick(): Promise<void> {
return new Promise((resolve) => {
if (this.hasClicked || !this.checkIsApple()) {
resolve();
return;
}
const node = new cc.Node();
node.addComponent(cc.BlockInputEvents);
const sprite = node.addComponent(cc.Sprite);
this.loadPng(this.playerStartButtonUrl).then((asset) => {
sprite.spriteFrame = new cc.SpriteFrame(
asset as unknown as cc.Texture2D
);
node.setPosition(cc.v2(0, 0));
cc.Canvas.instance.node.addChild(node);
node.once(cc.Node.EventType.TOUCH_END, () => {
node.destroy();
this.hasClicked = true;
resolve();
});
});
});
}
private checkIsApple() {
return /iphone|ipad|ios|mac/gi.test(navigator.userAgent.toLowerCase());
}
private getVideoDom(url: string) {
const list = document.getElementsByClassName(this.CLASS_NAME);
for (let i = 0; i < list.length; i++) {
const video = list[i];
if (url == video["src"]) {
return video;
}
}
return null;
}
}
使用样例:
import VideoPlayerManage from "./VideoPlayerManage";
const { ccclass } = cc._decorator;
@ccclass
export default class Index extends cc.Component {
private list = [
"http://localhost:3000/light1.mp4",
"http://localhost:3000/light2.mp4",
];
private index = 0;
protected async onLoad(): Promise<void> {
await VideoPlayerManage.instance.init(
this.list.map((url) => {
return {
tag: url,
url,
width: this.node.width,
height: this.node.height,
isLoop: true,
endCallback: () => {
console.log("end");
},
};
}),
{
poster: "",
playerStartButtonUrl: "http://localhost:3000/head1.png",
}
);
this.playByIndex();
this.node.on(cc.Node.EventType.TOUCH_END, () => {
this.playByIndex();
});
}
playByIndex() {
this.index++;
if (this.index >= this.list.length) {
this.index = 0;
}
VideoPlayerManage.instance.play(this.list[this.index]);
}
}