/**
 * 对外输出接口
 */
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]);
    }
}

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部