从0开始写一个播放器系列-核心类实现

核心类实现

提示: 谷歌浏览器中canvas的drawImage方法有限制,不支持跨域操作,所以video文件必须是本域名下的,emmm好像有点废柴的样子

核心类中主要实现的是创建弹幕, 创建标题, 创建控制条, video的渲染等核心操作

所需要用到弹幕类,控制条类,标题类以及基本数据类型,工具类几个文件

我在写这篇博客的时候还没有完成弹幕类和标题类的实现,所以写到弹幕类和标题类这两篇博客时会提及在核心类中加减的代码

核心类中所需要实现的功能

  • 在传入的父元素中创建 video标签, 中转canvas标签, 最终渲染的canvas标签

  • 实时渲染video标签到中转canvas标签和最终渲染的canvas标签中

  • 在开发过程中我发现canvas渲染video标签会产生画面变形,所以还需要计算video标签渲染的大小

了解以上需求就可以开始编码了

第一步: 创建Core类

class Core{}

根据上方需求开始丰富core类

需要保存的变量

  • 外部传入的父元素
    • 外部传入的父元素高度
    • 外部传入的父元素宽度
  • 创建元素的包裹父元素
  • video标签
    • 视频原始高度
    • 视频原始宽度
  • 中转canvas标签
    • 中转canvas的getContext对象
    • 中转canvas的绘画宽度
    • 中转canvas的绘画高度
    • 中转canvas的绘画Top值
    • 中转canvas的绘画Left值
  • 最终渲染canvas标签
    • 最终渲染canvas的getContext对象
  • 基本配置项
  • url列表
    • url列表当前的下标
    • url列表最大下标
  • 渲染函数requestAnimationFrame的id

开始创建变量

class Core{
    // 传入的父级dom节点
    parentDom: HTMLElement = null;
    p_width: number = null;
    p_height: number = null;

    //创建元素的包裹父元素
    lp_player_box:HTMLElement = document.createElement("div");
    // 生成子级dom
    // video Dom节点
    videoDom: HTMLVideoElement = document.createElement('video');
    video_width: number = 0;
    video_height: number = 0;

    // 中转canvas节点
    centerCanvasDom: HTMLCanvasElement = document.createElement("canvas");
    centerCan = this.centerCanvasDom.getContext("2d");
    drup_width: number = null
    drup_height: number = null
    drup_top: number = null
    drup_left: number = null
    // 最后输出的canvas节点
    finalCanvasDom: HTMLCanvasElement = document.createElement("canvas");
    finalCan = this.finalCanvasDom.getContext("2d");

    // 控制条dom
    controller: Controller = undefined;
    // UrlList
    baseUrlList: UrlListType = undefined;
    // 下方两个变量可以用来标识用户传入的是字符串还是数组
    selectUrlListIndex: number = null; // 当前选中的urlList的下标
    maxListindex:number = null;  // urlList的最大下标

    // 基本设置
    baseOptions: optionsFormat = {
        barrageShow: true, // 是否开启弹幕功能, 默认为true
        barrageList: [], // 弹幕数组默认为空
        barrageScope: 1500, // 默认为1500毫秒, 在可视区域停留时间
        title: {// 标题
            show: true, // 标题是否显示
            label: 'Title' // 标题显示内容
        },
        // 控制条显示
        controller: true, // 默认显示
        // 控制条配置项, 如果用户手动指定则按照用户手动指定的解析
        controllerOption: {
            play: true, // 播放按钮, 默认为true
            next: false, // 下一个按钮, 默认为false, 如果传入的videoUrl的格式为2或者4的话则默认为true
            clarity: false, //选择清晰度, 默认为false, 如果传入的videoUrl格式为3,4为true
            volume: true, // 音量默认为true
            playbackRate: true, // 播放速度默认为true
            playbackRateList: [
                {speed: 0.5, label: "0.5x"},
                {speed: 0.75, label: "0.75x"},
                {speed: 1.0, label: "1.0x",default:true},
                {speed: 1.25, label: "1.25x"},
                {speed: 1.5, label: "1.5x"},
                {speed: 2.0, label: "2.0x"},
            ], // 播放速度数组默认值
            togglePip: true, // 画中画 默认true
            documentFull: true, // 网页全屏 默认true
            windowFull: true, // 全屏 默认true
            Mirror: false // 镜像 默认false
        },
        _parentSelf:this // 将Core本身传递给引用类,方便操作
    };

    //渲染canvas的 requestAnimationFrameID
    reqAFId:number = null;
}

第二步: 实现构造函数

在构造函数中进行初始化操作

需要初始化的是上方为null的变量以及将创建好的标签插入到dom中

constructor(parentNode: HTMLElement, urlList: UrlListType, options?: optionsFormat) {
        // 初始化外部传入的dom
        this.parentDom = parentNode;
        this.p_width = this.parentDom.offsetWidth;
        this.p_height = this.parentDom.offsetHeight;
        this.baseUrlList = urlList;
        // 合并配置项
        Object.assign(this.baseOptions, options)
        // 判断传入的是否是数组
        if(whatUrlType(urlList) === 2){
            this.selectUrlListIndex = 0;
            this.maxListindex = urlList.length-1
            // 设置子元素的样式以及属性
            this.setChildDom(
                typeof this.baseUrlList[this.selectUrlListIndex]=="string" ?
                (this.baseUrlList as Array<string>)[this.selectUrlListIndex] : this.getUrl(<urlFormat1>this.baseUrlList[this.selectUrlListIndex])
            );
        }else{
            this.setChildDom(urlList as string); // 断言取消报错
        }
        // 创建控制条
        this.controller = new Controller(this.videoDom, this.lp_player_box , this.baseOptions);
        // 插入到传入元素中
        this.joinHtml();
        // 给video添加事件
        this.videoBindEvent();
    }
setChildDom 函数实现 : 设置创建出来的元素的数据
setChildDom(urlList: string): void {
    	// 设置创建元素的包裹父元素
        this.lp_player_box.classList.add("dp-video-player")
        styleList(this.lp_player_box,{
            width: this.p_width + "px",
            height: this.p_height + "px",
            background:'#000',
            position:"relative",
            overflowL:"hidden"
        })

		// video标签和中转canvas不需要显示出来所以都设置了display:none
		// 设置video标签
        styleList(this.videoDom, {width: this.p_width + "px", height: this.p_height + "px", display: 'none'})
        this.videoDom.src = urlList;
        this.videoDom.autoplay = true;  // 设置自动播放
        this.videoDom.controls = true;  // 设置显示控制条
        this.videoDom.muted = true;     // 设置静音

		// 设置中转canvas标签
		// 注意canvas标签的宽度和高度不要使用css操作, 会导致canvas显示变形,模糊等问题
        this.centerCanvasDom.width = this.p_width;
        this.centerCanvasDom.height = this.p_height;
        styleList(this.centerCanvasDom, {display: 'none'})
		// 设置最终渲染的canvas标签
        this.finalCanvasDom.width = this.p_width;
        this.finalCanvasDom.height = this.p_height;
    }
joinHtml函数实现: 元素插入html中
joinHtml(): void {
    // 这个函数就很简单了
    this.videoDom = this.lp_player_box.appendChild(this.videoDom);
    this.centerCanvasDom = this.lp_player_box.appendChild(this.centerCanvasDom);
    this.finalCanvasDom = this.lp_player_box.appendChild(this.finalCanvasDom);
    this.lp_player_box.appendChild(this.controller.getElement());
    this.parentDom.appendChild(this.lp_player_box)
}
videoBindEvent函数实现: 给video绑定事件处理函数
videoBindEvent() {
    this.videoDom.addEventListener('canplay', () => {
        // 在video可以播放的时候获取video的原始宽高
        this.video_width = this.videoDom.videoWidth;
        this.video_height = this.videoDom.videoHeight;
        // 重新计算绘画位置并且重绘
        this.recalculateDrawingPosition(false)

    })
    this.videoDom.addEventListener("play", () => {
        // video开始播放的时候进行实时绘制
        this.listenVideo()
    })
    this.videoDom.addEventListener("pause", () => {
        // video暂停的时候删除实时绘制函数,节省内存,避免造成没有必要浪费
        cancelAnimationFrame(this.reqAFId);
    })

    window.addEventListener("resize", () => {
        // window窗口大小改变的时候进行计算绘画位置并且重绘
        this.recalculateDrawingPosition(false)
    })
}
listenVideo函数实现: 实时绘制video
listenVideo(): void {
    // 如果video的真实宽高不存在那么反复调用本函数,直到获取到video的真实宽高
    if (!this.video_width && !this.video_height) {
        this.reqAFId = requestAnimationFrame(this.listenVideo.bind(this));
        return;
    }
    // 计算绘图位置, 不计算会导致渲染出来的画面变形
    this.countWhereDrop()
    // 中转canvas清屏
    this.centerCan.clearRect(this.drup_left, this.drup_top, this.drup_width, this.drup_height)
	// 中转canvas绘制video中的内容
    this.centerCan.drawImage(this.videoDom, this.drup_left, this.drup_top, this.drup_width, this.drup_height);
	// 获取中转canvas中的像素点数据
    let imageData: ImageData = this.centerCan.getImageData(0, 0, this.p_width, this.p_height);
	// 将像素点数据放置入最终渲染的canvas中
    this.finalCan.putImageData(imageData, 0, 0)
	// 使用requestAnimationFrame函数进行每次屏幕刷新渲染
	// 至于为什么要bind(this)....requestAnimationFrame函数中的作用域似乎会发生改变,会导致this指向不是Core,明确指向Core下面的代码才可以正常运行
    this.reqAFId = requestAnimationFrame(this.listenVideo.bind(this));
}
countWhereDrop函数实现: 重新计算绘画位置
countWhereDrop(flag: boolean = true): void {
    // 做节流处理
    if ((flag && this.p_width == this.lp_player_box.offsetWidth && this.p_width != null) || (flag && this.p_height == this.lp_player_box.offsetHeight && this.p_height != null)) {
        return
    }

    this.p_width = this.lp_player_box.offsetWidth;
    this.p_height = this.lp_player_box.offsetHeight;
    this.centerCanvasDom.width = this.p_width;
    this.centerCanvasDom.height = this.p_height;
    this.finalCanvasDom.width = this.p_width;
    this.finalCanvasDom.height = this.p_height;

    /*
    * 需要保证比例不变
    * 公式  变形值x = 原始值y * 变形值y / 原始值 x
    * 假设原始为 240 w , 300 h
    *    变形为 500 w , x h
    *       240 / 300 == 500 / x
    *       x = (300 * 500) / 240
    * */

    // 竖屏
    if (this.video_height > this.video_width) {
        this.drup_top = 0
        this.drup_height = this.p_height;
        this.drup_width = this.video_width * (this.p_height / this.video_height);
        this.drup_left = Math.abs((this.p_width - this.drup_width)) / 2;
    } else {
        // 横屏
        this.drup_left = 0;
        this.drup_width = this.p_width;
        this.drup_height = this.video_height * (this.p_width / this.video_width);
        this.drup_top = Math.abs((this.p_height - this.drup_height)) / 2
    }

    // 如果横屏的放到画面上的高度比p_height大的话以竖屏处理
    if (this.drup_height > this.p_height) {
        this.drup_top = 0
        this.drup_height = this.p_height;
        this.drup_width = this.video_width * (this.p_height / this.video_height);
        this.drup_left = Math.abs((this.p_width - this.drup_width)) / 2;
    }
}
recalculateDrawingPosition函数实现: 重新计算绘画位置并且重绘
recalculateDrawingPosition(flag: boolean = true) {
    // 这里的flag为是否强制刷新计算位置
    this.countWhereDrop(flag);
    this.listenVideo()
    setTimeout(() => {
        cancelAnimationFrame(this.reqAFId);
    })
}
getUrl函数实现:获取defaultId对应的url
getUrl(item: urlFormat1): string {
    let id = item.defaultId;
    let selectList: Array<urlFormat2> = item.urlList.filter((obj: urlFormat2) => {
        return obj.id === id;
    })
    return selectList[0].url
}
changeVideoSrc:修改video的src属性

该方法目前在Core类中没有什么作用,但是在控制条类中就有作用了,先摆在这里把

changeVideoSrc() {
    // 获取src地址
    let src = typeof this.baseUrlList[this.selectUrlListIndex] == "string" ? (this.baseUrlList as Array<string>)[this.selectUrlListIndex] : this.getUrl(<urlFormat1>this.baseUrlList[this.selectUrlListIndex])
    // 设置src地址
    this.videoDom.src = src;
    // 重载控制条  该方法在控制条类中介绍
    this.controller.unload();
    // 将控制条重新插入到dom中
    this.lp_player_box.appendChild(this.controller.getElement());
}

总结:

核心类中只负责渲染标签,渲染canvas,和改变video的src

剩下的在其他类中实现

核心类代码

import {
    urlFormat1, urlFormat2,
    optionsFormat, UrlListType
} from "./baseType"
import Controller from "./creatController";
import {whatUrlType, styleList} from "./utils"

export default class Core {
    // 传入的父级dom节点
    parentDom: HTMLElement = null;
    p_width: number = null;
    p_height: number = null;

    lp_player_box: HTMLElement = document.createElement("div");
    // 生成子级dom
    // video Dom节点
    videoDom: HTMLVideoElement = document.createElement('video');
    video_width: number = 0;
    video_height: number = 0;

    // 中转canvas节点
    centerCanvasDom: HTMLCanvasElement = document.createElement("canvas");
    centerCan = this.centerCanvasDom.getContext("2d");
    drup_width: number = null
    drup_height: number = null
    drup_top: number = null
    drup_left: number = null
    // 最后输出的canvas节点
    finalCanvasDom: HTMLCanvasElement = document.createElement("canvas");
    finalCan = this.finalCanvasDom.getContext("2d");

    // 控制条dom
    controller: Controller = undefined;
    // baseUrlList
    baseUrlList: UrlListType = undefined;
    selectUrlListIndex: number = null;
    maxListindex: number = null

    // 基本设置
    baseOptions: optionsFormat = {
        barrageShow: true, // 是否开启弹幕功能, 默认为true
        barrageList: [], // 弹幕数组默认为空
        barrageScope: 1500, // 默认为1500毫秒, 在可视区域停留时间
        title: {// 标题
            show: true, // 标题是否显示
            label: 'Title' // 标题显示内容
        },
        // 控制条显示
        controller: true, // 默认显示
        // 控制条配置项, 如果用户手动指定则按照用户手动指定的解析
        controllerOption: {
            play: true, // 播放按钮, 默认为true
            next: false, // 下一个按钮, 默认为false, 如果传入的videoUrl的格式为2或者4的话则默认为true
            clarity: false, //选择清晰度, 默认为false, 如果传入的videoUrl格式为3,4为true
            volume: true, // 音量默认为true
            playbackRate: true, // 播放速度默认为true
            playbackRateList: [
                {speed: 0.5, label: "0.5x"},
                {speed: 0.75, label: "0.75x"},
                {speed: 1.0, label: "1.0x", default: true},
                {speed: 1.25, label: "1.25x"},
                {speed: 1.5, label: "1.5x"},
                {speed: 2.0, label: "2.0x"},
            ], // 播放速度数组默认值
            togglePip: true, // 画中画 默认true
            documentFull: true, // 网页全屏 默认true
            windowFull: true, // 全屏 默认true
            Mirror: false // 镜像 默认false
        },
        _parentSelf: this
    };

    //渲染canvas的 requestAnimationFrameID
    reqAFId: number = null;


    constructor(parentNode: HTMLElement, urlList: UrlListType, options?: optionsFormat) {
        // 初始化外部传入的dom
        this.parentDom = parentNode;
        this.p_width = this.parentDom.offsetWidth;
        this.p_height = this.parentDom.offsetHeight;
        this.baseUrlList = urlList;
        // 合并配置项
        Object.assign(this.baseOptions, options)
        // 判断传入的是否是数组
        if (whatUrlType(urlList) === 2) {
            this.selectUrlListIndex = 0;
            this.maxListindex = urlList.length - 1
            // 设置子元素的样式以及属性
            this.setChildDom(
                typeof this.baseUrlList[this.selectUrlListIndex] == "string" ?
                    (this.baseUrlList as Array<string>)[this.selectUrlListIndex] : this.getUrl(<urlFormat1>this.baseUrlList[this.selectUrlListIndex])
            );
        } else {
            this.setChildDom(urlList as string);
        }
        // 创建控制条
        this.controller = new Controller(this.videoDom, this.lp_player_box, this.baseOptions);

        // 插入到传入元素中
        this.joinHtml();
        // 给video添加事件
        this.videoBindEvent();
    }

    // 元素插入html中
    joinHtml(): void {
        this.lp_player_box.appendChild(this.videoDom);
        this.lp_player_box.appendChild(this.centerCanvasDom);
        this.lp_player_box.appendChild(this.finalCanvasDom);
        this.lp_player_box.appendChild(this.controller.getElement());
        this.parentDom.appendChild(this.lp_player_box)
    }

    // 设置创建出来的元素的数据
    setChildDom(urlList: string): void {
        this.lp_player_box.classList.add("dp-video-player")
        styleList(this.lp_player_box, {
            width: "100%",
            height: "100%",
            background: '#000',
            position: "relative",
            overflowL: "hidden"
        })

        styleList(this.videoDom, {width: this.p_width + "px", height: this.p_height + "px", display: 'none'})
        this.videoDom.src = urlList as string; // 使用断言取消报错
        this.videoDom.autoplay = true;
        this.videoDom.controls = true;
        this.videoDom.muted = true;


        this.centerCanvasDom.width = this.p_width;
        this.centerCanvasDom.height = this.p_height;
        styleList(this.centerCanvasDom, {display: 'none'})

        this.finalCanvasDom.width = this.p_width;
        this.finalCanvasDom.height = this.p_height;
    }

    // 开始监听video
    listenVideo(): void {
        if (!this.video_width && !this.video_height) {
            this.reqAFId = requestAnimationFrame(this.listenVideo.bind(this));
            return;
        }
        this.countWhereDrop()
        this.centerCan.clearRect(this.drup_left, this.drup_top, this.drup_width, this.drup_height)
        this.centerCan.drawImage(this.videoDom, this.drup_left, this.drup_top, this.drup_width, this.drup_height);
        let imageData: ImageData = this.centerCan.getImageData(0, 0, this.p_width, this.p_height);
        this.finalCan.putImageData(imageData, 0, 0)
        this.reqAFId = requestAnimationFrame(this.listenVideo.bind(this));
    }

    // 给video添加事件处理函数
    videoBindEvent() {
        this.videoDom.addEventListener('canplay', () => {
            this.video_width = this.videoDom.videoWidth;
            this.video_height = this.videoDom.videoHeight;
            this.recalculateDrawingPosition(false)

        })
        this.videoDom.addEventListener("play", () => {
            this.listenVideo()
        })
        this.videoDom.addEventListener("pause", () => {
            cancelAnimationFrame(this.reqAFId);
        })

        window.addEventListener("resize", () => {
            this.recalculateDrawingPosition(false)
        })
    }

    // 计算绘图位置, 不计算会导致渲染出来的画面变形
    countWhereDrop(flag: boolean = true): void {
        if ((flag && this.p_width == this.lp_player_box.offsetWidth && this.p_width != null) || (flag && this.p_height == this.lp_player_box.offsetHeight && this.p_height != null)) {
            return
        }

        this.p_width = this.lp_player_box.offsetWidth;
        this.p_height = this.lp_player_box.offsetHeight;
        this.centerCanvasDom.width = this.p_width;
        this.centerCanvasDom.height = this.p_height;
        this.finalCanvasDom.width = this.p_width;
        this.finalCanvasDom.height = this.p_height;

        /*
        * 需要保证比例不变
        * 公式  变形值x = 原始值y * 变形值y / 原始值 x
        * 假设原始为 240 w , 300 h
        *    变形为 500 w , x h
        *       240 / 300 == 500 / x
        *       x = (300 * 500) / 240
        * */

        // 竖屏
        if (this.video_height > this.video_width) {
            this.drup_top = 0
            this.drup_height = this.p_height;
            this.drup_width = this.video_width * (this.p_height / this.video_height);
            this.drup_left = Math.abs((this.p_width - this.drup_width)) / 2;
        } else {
            // 横屏
            this.drup_left = 0;
            this.drup_width = this.p_width;
            this.drup_height = this.video_height * (this.p_width / this.video_width);
            this.drup_top = Math.abs((this.p_height - this.drup_height)) / 2
        }

        // 如果横屏的放到画面上的高度比p_height大的话以竖屏处理
        if (this.drup_height > this.p_height) {
            this.drup_top = 0
            this.drup_height = this.p_height;
            this.drup_width = this.video_width * (this.p_height / this.video_height);
            this.drup_left = Math.abs((this.p_width - this.drup_width)) / 2;
        }
    }

    // 重新计算绘画位置并且重绘
    recalculateDrawingPosition(flag: boolean = true) {
        this.countWhereDrop(flag);
        this.listenVideo()
        setTimeout(() => {
            cancelAnimationFrame(this.reqAFId);
        })
    }

    // 修改video src
    changeVideoSrc() {
        let src = typeof this.baseUrlList[this.selectUrlListIndex] == "string" ? (this.baseUrlList as Array<string>)[this.selectUrlListIndex] : this.getUrl(<urlFormat1>this.baseUrlList[this.selectUrlListIndex])
        this.videoDom.src = src;
        this.controller.unload();
        this.lp_player_box.appendChild(this.controller.getElement());
    }

    // 获取url
    getUrl(item: urlFormat1): string {
        let id = item.defaultId;
        let selectList: Array<urlFormat2> = item.urlList.filter((obj: urlFormat2) => {
            return obj.id === id;
        })
        return selectList[0].url
    }

    // 创建拓展接口, 后续可能会用到 先写上
    /*
    import VideoL from "./core/index"
    VideoL.extend("add",function(num1,num2){
        return num1+num2
    })
    * */
    static extend(name: string | number, fn: Function) {
        Core.prototype[name] = fn
        if (this.prototype[name]) {
            return !!1;
        } else {
            return false;
        }
    }
}

GitHub地址: https://github.com/lidppp/dp-video
本人博客: https://www.lidppp.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值