first commit
This commit is contained in:
1
src/core/.pydio
Normal file
1
src/core/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
1df3ad8a-5c58-442f-93c0-3e72b9fd3577
|
||||
1
src/core/annotation/.pydio
Normal file
1
src/core/annotation/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
966472fa-a3be-4729-a71b-ae988035f1e7
|
||||
18
src/core/annotation/Dom.annotation.ts
Normal file
18
src/core/annotation/Dom.annotation.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import OptionParamsModel from "../model/OptionParams.model";
|
||||
import { ClassName } from "../config/ClassName.config";
|
||||
|
||||
export function Dom() {
|
||||
return function (target: any, propertyName: string) {
|
||||
setTimeout(()=> {
|
||||
try {
|
||||
target[propertyName] = OptionParamsModel.VideoDom.querySelector(`.${ClassName[propertyName]}`)
|
||||
} catch (e) {
|
||||
throw new Error(`错误信息:需要绑定事件的类名:${ClassName[propertyName]}不存在
|
||||
1:请检查类名是否存在
|
||||
2:删除 ui/*.ui.ts 代码中对应的 Dom() 装饰器 和 方法
|
||||
`)
|
||||
}
|
||||
}
|
||||
,300)
|
||||
}
|
||||
}
|
||||
36
src/core/annotation/Ioc.annotation.ts
Normal file
36
src/core/annotation/Ioc.annotation.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import IocModel from "../model/Ioc.model";
|
||||
|
||||
/**
|
||||
* 收集类依赖
|
||||
* @constructor
|
||||
*/
|
||||
export function Injectable() {
|
||||
return (_constructor: { new (...args: any[]): {}; }) => {
|
||||
if(IocModel.classPool.indexOf(_constructor) !== -1) {
|
||||
throw new Error('无需重复收集类');
|
||||
} else {
|
||||
//注册
|
||||
IocModel.classPool = [_constructor]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将类依赖实例化然后注入到被装饰的属性中
|
||||
* @constructor
|
||||
*/
|
||||
export function Inject() {
|
||||
return function (target: any, propertyName: string) {
|
||||
/**
|
||||
* 使用 reflect-metadata 提供的内置 类型元数据键 design:type 通过反射拿到被装饰属性的类型
|
||||
* 也就是 类属性要实例化 的 service 类
|
||||
*/
|
||||
const propertyType: any = Reflect.getMetadata('design:type', target, propertyName);
|
||||
if (IocModel.classPool.indexOf(propertyType) == -1) {
|
||||
throw new Error('被装饰的属性所属的变量类型类,没有被装饰器@Injectable()注入,请检查!');
|
||||
} else {
|
||||
// 从存取器的数组中通过下标取出被装饰属性对应的service类,然后实例化这个类,在放入被装饰的属性中
|
||||
target[propertyName] = new (IocModel.classPool[IocModel.classPool.indexOf(propertyType)])()
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/core/config/.pydio
Normal file
1
src/core/config/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
fbfd9291-1d66-4dcb-bcd8-abbe474d7c84
|
||||
22
src/core/config/ClassName.config.ts
Normal file
22
src/core/config/ClassName.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Config } from "./index.config";
|
||||
|
||||
export enum ClassName {
|
||||
// 播放按钮的div class 名称
|
||||
Play = "icon-bofang",
|
||||
Video = "a_video",
|
||||
Pause = "icon-tingzhi1",
|
||||
Loop = "icon-xunhuan",
|
||||
Stop = "icon-tingzhi1",
|
||||
Active = "active",
|
||||
High = "icon-huaban-",
|
||||
PictureInPicture = "icon-ai223",
|
||||
Rotate = "icon-xuanzhuan1",
|
||||
Image = "icon-jingxiang2",
|
||||
ScreenShot = "icon-jietu_huaban",
|
||||
VideoProgress = "a_progress",
|
||||
VideoTime = "a_videotime",
|
||||
Speed = "icon-suduspeed8",
|
||||
FullScreen = "icon-quanping_huaban",
|
||||
playList = "icon-liebiao",
|
||||
SideMenu = "a_side_menu"
|
||||
}
|
||||
9
src/core/config/VideoMimeType.config.ts
Normal file
9
src/core/config/VideoMimeType.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export enum VideoMimeType {
|
||||
HLS = "application/x-mpegurl",
|
||||
TS = "video/MP2T",
|
||||
MP4 = "video/mp4",
|
||||
STREA = "application/octet-stream",
|
||||
FLV = 'video/x-flv',
|
||||
MOV = 'video/quicktime',
|
||||
AVI = 'video/x-msvideo',
|
||||
}
|
||||
61
src/core/config/index.config.ts
Normal file
61
src/core/config/index.config.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { OptionParamsTypes } from "../types/OptionParams.types";
|
||||
|
||||
export class Config {
|
||||
public static DefalutOptionParams: OptionParamsTypes = {
|
||||
el: '#app', // 父节点
|
||||
url: 'http://demo-videos.qnsdk.com/movies/qiniu.mp4', // 视频地址,参数2:[{title:'高清', url: ''},{title: '普清', url: ''}]
|
||||
title: '欢迎使用 AVideo 播放器!',
|
||||
layout: 'DefaultLayout',// 选择的播放器布局主题
|
||||
width: '100%', // 高度
|
||||
height: '400px', // 宽度
|
||||
autoplay: false, // 是否自动播放
|
||||
muted: false, // 是否静音 true 静音,false 不静音
|
||||
volume: 1, // 初始音量 0 ~ 1 之间
|
||||
poster: '',// 视频预览图地址
|
||||
playList: [{title:'1',url:'222'}], // 显示视频播放列表
|
||||
controls: { // 配置视频底部 播放控件
|
||||
image: true, // 是否需要镜像功能
|
||||
setting: true, // 是否需要设置功能
|
||||
rotate: true, // 是否开启旋转,开启后,视频旋转默认顺时针90°度数。参数还支持传入正负的度数例如:-30,将会逆时针使用用户传入度数进行旋转
|
||||
play: true, // 是否需要播放按钮
|
||||
loop: true, // 是否需要循环播放按钮
|
||||
speed: true, // 是否需要倍速,如果传入数组,类似:[0.3, 0.5, 1, 1.5, 2, 3],那么采用用户传入
|
||||
FullScreen: true, // 是否需要全屏按钮
|
||||
down: true, // 是否需要下载按钮
|
||||
PictureInPicture: true, // 是否需要画中画按钮
|
||||
memory: true, // 是否开启记忆播放 参数2:{ tips: '是否继续从2:00开始播放' } tips 自定义提示文字
|
||||
ScreenShot: true, // 是否需要截图,
|
||||
theme: true, // 是否显示皮肤切换的按钮
|
||||
progressMarkers: [],
|
||||
vip: {},
|
||||
},
|
||||
paused: (video)=> {}, // 视频被暂停的回调
|
||||
play: (video)=> {}, // 视频开始播放的回调
|
||||
finish: (video)=> {}, // 视频播放完成的回调
|
||||
error: (video)=> {}, // 视频播放出错的回调
|
||||
timeupdate: (video)=> {}, // 视频播放时间更新的回调
|
||||
};
|
||||
|
||||
public static IconConfig = {
|
||||
topIcon: [
|
||||
{ title: '下载', icon: 'icon-down1', key: 'down' },
|
||||
{ title: '列表', icon: 'icon-liebiao', key: 'playList' },
|
||||
{ title: '设置', icon: 'icon-setting', key: 'setting' },
|
||||
],
|
||||
leftIcon: [
|
||||
{ title: '播放', icon: 'icon-bofang', key: 'play' },
|
||||
{ title: '循环播放', icon: 'icon-xunhuan', key: 'loop' },
|
||||
],
|
||||
rightIcon: [
|
||||
{ title: '皮肤', icon: 'icon-jingzi', key: 'theme' },
|
||||
{ title: '高清', icon: 'icon-huaban-', key: 'high' },
|
||||
{ title: '旋转', icon: 'icon-xuanzhuan1', key: 'rotate' },
|
||||
{ title: '镜像', icon: 'icon-jingxiang2', key: 'image' },
|
||||
{ title: '截图', icon: 'icon-jietu_huaban', key: 'ScreenShot' },
|
||||
{ title: '倍速', icon: 'icon-suduspeed8', key: 'speed' },
|
||||
{ title: '画中画', icon: 'icon-ai223', key: 'PictureInPicture' },
|
||||
{ title: '全屏', icon: 'icon-quanping_huaban', key: 'FullScreen' },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
43
src/core/init.ts
Normal file
43
src/core/init.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { OptionParamsTypes } from "./types/OptionParams.types";
|
||||
import OptionParamsModel from "./model/OptionParams.model";
|
||||
import { Config } from "./config/index.config";
|
||||
import { ApplicationUI } from "./ui/index.ui";
|
||||
import { Inject, Injectable } from "./annotation/Ioc.annotation";
|
||||
|
||||
/**
|
||||
* 核心必传参数的校验
|
||||
*/
|
||||
export class Init {
|
||||
|
||||
@Inject()
|
||||
private ApplicationUI!: ApplicationUI;
|
||||
private OptionParams: OptionParamsTypes;
|
||||
|
||||
constructor(OptionParams: OptionParamsTypes) {
|
||||
this.OptionParams = OptionParams;
|
||||
this.checkParams()
|
||||
}
|
||||
|
||||
public checkParams(): void {
|
||||
if (this.OptionParams.hasOwnProperty("el") && this.OptionParams.hasOwnProperty("url")) {
|
||||
|
||||
if (this.OptionParams.controls && Object.keys(this.OptionParams.controls).length == 0) {
|
||||
throw new Error("请勿传入空controls的配置")
|
||||
}
|
||||
|
||||
this.OptionParams.playList === undefined
|
||||
? delete Config.DefalutOptionParams.playList
|
||||
: null
|
||||
|
||||
// 将用户传入的参数和默认参数进行合并,如果值冲突,优先采用用户的配置来覆盖默认值
|
||||
Object.assign(Config.DefalutOptionParams, this.OptionParams)
|
||||
// 保存配置到存取器,供全局使用
|
||||
OptionParamsModel.OptionParams = Config.DefalutOptionParams;
|
||||
|
||||
this.ApplicationUI.GenerateDom();
|
||||
} else {
|
||||
throw new Error("请检查必传参数:el 和 url,是否传入!")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
1
src/core/model/.pydio
Normal file
1
src/core/model/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
3564163c-013d-4d8c-94bc-a306dd223191
|
||||
13
src/core/model/Ioc.model.ts
Normal file
13
src/core/model/Ioc.model.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
class IocModel {
|
||||
private _classPool: Array<{ new (...args: any[]): {}; }> = [];
|
||||
|
||||
get classPool(): Array<{ new(...args: any[]): {} }> {
|
||||
return this._classPool;
|
||||
}
|
||||
|
||||
set classPool(value: Array<{ new(...args: any[]): {} }>) {
|
||||
this._classPool = [...value, ...this._classPool]
|
||||
}
|
||||
}
|
||||
|
||||
export default new IocModel();
|
||||
24
src/core/model/OptionParams.model.ts
Normal file
24
src/core/model/OptionParams.model.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { OptionParamsTypes } from "../types/OptionParams.types";
|
||||
|
||||
class OptionParamsModel {
|
||||
private _OptionParams: OptionParamsTypes;
|
||||
private _VideoDom: HTMLElement;
|
||||
|
||||
get VideoDom(): HTMLElement {
|
||||
return this._VideoDom;
|
||||
}
|
||||
|
||||
set VideoDom(value: HTMLElement) {
|
||||
this._VideoDom = value;
|
||||
}
|
||||
|
||||
get OptionParams(): OptionParamsTypes {
|
||||
return this._OptionParams;
|
||||
}
|
||||
|
||||
set OptionParams(value: OptionParamsTypes) {
|
||||
this._OptionParams = value;
|
||||
}
|
||||
}
|
||||
|
||||
export default new OptionParamsModel();
|
||||
1
src/core/types/.pydio
Normal file
1
src/core/types/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
df13c4ef-91b6-4544-a480-23a706749d0a
|
||||
62
src/core/types/OptionParams.types.ts
Normal file
62
src/core/types/OptionParams.types.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// 播放地址
|
||||
interface UrlItem {
|
||||
title: string,
|
||||
url: string
|
||||
}
|
||||
|
||||
// 记忆打点配置
|
||||
interface MemoryItem {
|
||||
tips: string
|
||||
}
|
||||
|
||||
// 视频打点 的配置数组
|
||||
interface progressMarkersItem {
|
||||
title: string,
|
||||
theme?: number,
|
||||
offset: number,
|
||||
img?: string,
|
||||
describe?: string
|
||||
}
|
||||
|
||||
// 视频底部ui控件
|
||||
export interface ControlsTypes {
|
||||
setting?: boolean,
|
||||
rotate?: boolean | number,
|
||||
image?: boolean,
|
||||
play?: boolean,
|
||||
loop?: boolean,
|
||||
speed?: boolean | number[],
|
||||
FullScreen?: boolean,
|
||||
down?: boolean,
|
||||
PictureInPicture?: boolean,
|
||||
memory?: boolean | MemoryItem,
|
||||
ScreenShot?: boolean,
|
||||
theme?: boolean,
|
||||
progressMarkers: Array<progressMarkersItem>,
|
||||
vip: {
|
||||
time?: number,
|
||||
tip?: string,
|
||||
popup?: string
|
||||
}
|
||||
}
|
||||
|
||||
// 主要配置
|
||||
export interface OptionParamsTypes {
|
||||
el: string,
|
||||
url: string | Array<UrlItem>,
|
||||
title: string,
|
||||
layout?: string,
|
||||
width?: string,
|
||||
height?: string,
|
||||
autoplay?: boolean,
|
||||
muted?: boolean,
|
||||
volume?: number,
|
||||
poster?: string,
|
||||
playList?: Array<UrlItem>,
|
||||
controls?: ControlsTypes,
|
||||
paused?: (video: HTMLMediaElement) => any;
|
||||
play?: (video: HTMLMediaElement) => any;
|
||||
finish?: (video: HTMLMediaElement) => any;
|
||||
error?: (video: HTMLMediaElement) => any;
|
||||
timeupdate?: (video: HTMLMediaElement) => any;
|
||||
}
|
||||
1
src/core/ui/.pydio
Normal file
1
src/core/ui/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
c86ae987-557c-4c45-ae05-8678ae14b9a5
|
||||
239
src/core/ui/controls.ui.ts
Normal file
239
src/core/ui/controls.ui.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import OptionParamsModel from "../model/OptionParams.model";
|
||||
import { Utils } from "../utils/index.utils";
|
||||
import { Inject, Injectable } from "../annotation/Ioc.annotation";
|
||||
import { Dom } from "../annotation/Dom.annotation";
|
||||
import {ClassName} from "../config/ClassName.config";
|
||||
|
||||
/**
|
||||
* 底部控件条的基础功能设置
|
||||
*/
|
||||
export class ControlsUi {
|
||||
|
||||
@Dom()
|
||||
private readonly Play!: HTMLElement;
|
||||
|
||||
@Dom()
|
||||
private readonly Video!: HTMLMediaElement & HTMLVideoElement;
|
||||
|
||||
@Dom()
|
||||
private readonly Loop!: HTMLElement;
|
||||
|
||||
@Dom()
|
||||
private readonly PictureInPicture!: HTMLElement;
|
||||
|
||||
@Dom()
|
||||
private readonly Rotate!: HTMLElement;
|
||||
|
||||
@Dom()
|
||||
private readonly Image!: HTMLLIElement;
|
||||
|
||||
@Dom()
|
||||
private readonly ScreenShot!: HTMLLIElement;
|
||||
|
||||
@Dom()
|
||||
private readonly Speed!: HTMLLIElement;
|
||||
|
||||
@Dom()
|
||||
private readonly FullScreen!: HTMLLIElement;
|
||||
|
||||
@Dom()
|
||||
private readonly playList!: HTMLLIElement;
|
||||
|
||||
@Inject()
|
||||
private readonly Utils!: Utils;
|
||||
|
||||
/**
|
||||
* 播放按钮被点击,播放视频
|
||||
* @constructor
|
||||
*/
|
||||
public BindEventPlay(): void {
|
||||
this.Play.onclick = async () => {
|
||||
this.Video.paused ? await this.Video.play() : await this.Video.pause()
|
||||
this.Play.classList.toggle(ClassName.Stop)
|
||||
}
|
||||
}
|
||||
|
||||
public BindEventPlayList(): void {
|
||||
let SideMenu = this.Utils.$(`.${ClassName.SideMenu}`);
|
||||
this.playList.onclick = async () => {
|
||||
if (SideMenu.style.right == "0px") {
|
||||
this.Utils.$(`.${ClassName.SideMenu}`).style.right = '-250px'
|
||||
} else {
|
||||
this.Utils.$(`.${ClassName.SideMenu}`).style.right = '0'
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 视频全屏
|
||||
* @constructor
|
||||
*/
|
||||
public BindEventFullScreen(): void {
|
||||
this.FullScreen.onclick = async () => {
|
||||
if (!document.fullscreenElement) {
|
||||
this.Video.parentElement.style.width = '100%'
|
||||
this.Video.parentElement.style.height = '100%'
|
||||
await document.documentElement.requestFullscreen();
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
this.Video.parentElement.style.width = OptionParamsModel.OptionParams.width
|
||||
this.Video.parentElement.style.height = OptionParamsModel.OptionParams.height
|
||||
await document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
this.FullScreen.classList.toggle(ClassName.Active)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 截图按钮
|
||||
* @constructor
|
||||
*/
|
||||
public BindEventScreenShot(): void {
|
||||
let scale = 0.65;
|
||||
this.ScreenShot.onclick = () => {
|
||||
var canvas = document.createElement("canvas");
|
||||
canvas.width = this.Video.videoWidth * scale;
|
||||
canvas.height = this.Video.videoHeight * scale;
|
||||
canvas.getContext('2d')
|
||||
.drawImage(this.Video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
this.Utils.downLoadFile(canvas.toDataURL('image/jpeg',1.0), 'image/jpeg')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 循环播放的开关
|
||||
* @constructor
|
||||
*/
|
||||
public BindEventLoop(): void {
|
||||
this.Loop.onclick = () => {
|
||||
this.Video.loop ? this.Video.loop = false : this.Video.loop = true
|
||||
this.Loop.classList.toggle(ClassName.Active)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 视频镜像功能
|
||||
* @constructor
|
||||
*/
|
||||
public BindEventImage(): void {
|
||||
// 延迟解决 ondblclick 时候会触发 onclick 的问题
|
||||
let ImageStatus: boolean = true, timeId = null; // true rotateY 镜像,false rotateX 镜像
|
||||
// 双击视频恢复
|
||||
this.Image.ondblclick = () => {
|
||||
clearTimeout(timeId);
|
||||
ImageStatus = true
|
||||
this.Video.style.cssText = `
|
||||
transform: rotateY(0deg);
|
||||
`
|
||||
}
|
||||
|
||||
// 单击镜像
|
||||
this.Image.onclick = () => {
|
||||
clearTimeout(timeId);
|
||||
timeId = setTimeout(() => {
|
||||
if (ImageStatus) {
|
||||
ImageStatus = false
|
||||
this.Video.style.cssText = `
|
||||
transform: rotateY(180deg);
|
||||
`
|
||||
} else {
|
||||
ImageStatus = true
|
||||
this.Video.style.cssText = `
|
||||
transform: rotateX(180deg);
|
||||
`
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO 视频倍速的功能 实现中...
|
||||
* @constructor
|
||||
*/
|
||||
public BindEventSpeed(): void {
|
||||
// 默认视频倍速
|
||||
let defaultSpeed: number[] = [0.5, 1.0, 1.25, 1.5, 2.0];
|
||||
|
||||
this.Speed.onclick = () => {
|
||||
// 如果传入的是数组
|
||||
if (Array.isArray(OptionParamsModel.OptionParams.controls.speed)) {
|
||||
defaultSpeed = OptionParamsModel.OptionParams.controls.speed;
|
||||
console.log("如果传入的是数组")
|
||||
|
||||
// 如果传入的是true,表示开启默认的倍速播放功能
|
||||
} else if (OptionParamsModel.OptionParams.controls.speed == true) {
|
||||
console.log("表示开启默认的倍速播放功能")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 画中画
|
||||
* @constructor
|
||||
*/
|
||||
public BindEventPictureInPicture(): void {
|
||||
this.PictureInPicture.onclick = () => {
|
||||
if (document.pictureInPictureElement == null) {
|
||||
this.Video.requestPictureInPicture();
|
||||
this.PictureInPicture.classList.add(ClassName.Active)
|
||||
} else {
|
||||
document.exitPictureInPicture();
|
||||
this.PictureInPicture.classList.remove(ClassName.Active)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听退出画中画功能
|
||||
*/
|
||||
this.Video.addEventListener('leavepictureinpicture', () => {
|
||||
this.PictureInPicture.classList.remove(ClassName.Active)
|
||||
this.Play.classList.toggle(ClassName.Stop)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 视频旋转
|
||||
* @constructor
|
||||
*/
|
||||
public BindEventRotate(): void {
|
||||
let currentRotate: number = 0, RotateNumber: number = 0;
|
||||
this.Rotate.onclick = () => {
|
||||
if (RotateNumber >= 4) {
|
||||
currentRotate = 0;
|
||||
RotateNumber = 0;
|
||||
}
|
||||
|
||||
currentRotate += <boolean>OptionParamsModel.OptionParams.controls.rotate == true
|
||||
? 90
|
||||
: <number>OptionParamsModel.OptionParams.controls.rotate;
|
||||
|
||||
RotateNumber++
|
||||
if (currentRotate == 180 || currentRotate == 360 ||
|
||||
currentRotate == -180 || currentRotate == -360) {
|
||||
|
||||
this.Video.style.cssText = `
|
||||
width: 100%;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%) rotate(${currentRotate}deg);
|
||||
`;
|
||||
|
||||
return false
|
||||
|
||||
} else {
|
||||
// 视频旋转 90°,270°后,视频的宽度 等于 父元素的高度
|
||||
this.Video.style.cssText = `
|
||||
width: ${OptionParamsModel.OptionParams.height};
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%) rotate(${currentRotate}deg);
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
120
src/core/ui/coreSettings.ui.ts
Normal file
120
src/core/ui/coreSettings.ui.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import {Dom} from "../annotation/Dom.annotation";
|
||||
import OptionParamsModel from "../model/OptionParams.model";
|
||||
import { ClassName } from "../config/ClassName.config";
|
||||
import { VideoMimeType } from "../config/VideoMimeType.config";
|
||||
import {Utils} from "../utils/index.utils";
|
||||
import {Inject} from "../annotation/Ioc.annotation";
|
||||
import * as Hls from "hls.js";
|
||||
|
||||
/**
|
||||
* 视频核心功能设置
|
||||
*/
|
||||
export class CoreSettingsUi {
|
||||
|
||||
@Dom()
|
||||
private readonly Play!: HTMLElement;
|
||||
|
||||
@Dom()
|
||||
private readonly Video!: HTMLVideoElement;
|
||||
|
||||
@Dom()
|
||||
private readonly High!: HTMLElement;
|
||||
|
||||
@Inject()
|
||||
private readonly Utils: Utils;
|
||||
|
||||
@Dom()
|
||||
private readonly VideoProgress!: HTMLInputElement;
|
||||
|
||||
/**
|
||||
* 设置视频预览图地址
|
||||
* @constructor
|
||||
*/
|
||||
public BindEventVideoPoster(): void {
|
||||
this.Video.poster = OptionParamsModel.OptionParams.poster
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染视频播放列表
|
||||
* @constructor
|
||||
*/
|
||||
public BindEventPlayVideoList(): void {
|
||||
let SideMenu: NodeListOf<HTMLDivElement> = document.querySelectorAll(`.${ClassName.SideMenu} .list_item`)
|
||||
SideMenu.forEach((v,index) => {
|
||||
v.onclick = () => {
|
||||
this.VideoProgress.style.backgroundSize = `0% 100%`;
|
||||
setTimeout(() => this.VideoProgress.value = String(0),1)
|
||||
this.VideoProgress.step = '0.000000000000000001'
|
||||
this.Video.src = OptionParamsModel.OptionParams.playList[index].url;
|
||||
this.Play.classList.remove(ClassName.Stop)
|
||||
this.Play.classList.add(ClassName.Play)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置视频宽高
|
||||
* @constructor
|
||||
*/
|
||||
public BindEventVideoSize(): void {
|
||||
this.Video.parentElement.style.width = OptionParamsModel.OptionParams.width
|
||||
this.Video.parentElement.style.height = OptionParamsModel.OptionParams.height
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置视频的自动播放 和 是否静音
|
||||
* @constructor
|
||||
*/
|
||||
public BindEventVideoAutoPlayAndMuted(): void {
|
||||
this.Video.autoplay = OptionParamsModel.OptionParams.autoplay;
|
||||
this.Video.muted = OptionParamsModel.OptionParams.muted;
|
||||
// 谷歌等众多浏览器禁止video标签有声的自动播放,所以这里如果想实现有声而且自动播放视频那么需要特殊处理
|
||||
// 前端用户需要开启 autoplay true 自动播放,muted true 静音
|
||||
if (OptionParamsModel.OptionParams.autoplay && OptionParamsModel.OptionParams.muted) {
|
||||
console.log("无声,自动播放")
|
||||
this.Play.classList.toggle(ClassName.Stop)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置单个视频地址 & 多个清晰度视频地址
|
||||
* @constructor
|
||||
*/
|
||||
public async BindEventEndedVideoSetUrl() {
|
||||
// 单个视频地址
|
||||
if (typeof OptionParamsModel.OptionParams.url === "string") {
|
||||
await this.Utils.CheckVideoType(<string>OptionParamsModel.OptionParams.url)
|
||||
// 多清晰度视频的数组
|
||||
} else {
|
||||
// 数组默认第一条视频作为播放视频
|
||||
await this.Utils.CheckVideoType(OptionParamsModel.OptionParams.url[0].url)
|
||||
|
||||
let UL: HTMLUListElement = document.createElement("ul");
|
||||
UL.className = "high";
|
||||
OptionParamsModel.OptionParams.url.map((val,index) => {
|
||||
let li: HTMLLIElement = document.createElement("li");
|
||||
li.textContent = val.title;
|
||||
// 默认第一个显示激活样式
|
||||
index == 0 ? li.classList.toggle(ClassName.Active) : null
|
||||
li.onclick = async () => {
|
||||
// 清除所有li激活类名
|
||||
li.parentElement.childNodes.forEach((s: HTMLLIElement)=> s.className = "")
|
||||
// 为点击的里高亮激活
|
||||
li.className = ClassName.Active
|
||||
// 切换视频地址
|
||||
await this.Utils.CheckVideoType(val.url)
|
||||
};
|
||||
// 填充li到ul中
|
||||
UL.appendChild(li);
|
||||
});
|
||||
// 鼠标悬浮 显示清晰度选择菜单
|
||||
this.High.onmouseover = () => UL.style.display = "block";
|
||||
this.High.onmouseout = () => UL.style.display = "none";
|
||||
// 填充ul到清晰度选择的图标中
|
||||
this.High.appendChild(UL);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
97
src/core/ui/eventListeners.ui.ts
Normal file
97
src/core/ui/eventListeners.ui.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import {Dom} from "../annotation/Dom.annotation";
|
||||
import OptionParamsModel from "../model/OptionParams.model";
|
||||
import { ClassName } from "../config/ClassName.config";
|
||||
import {Inject} from "../annotation/Ioc.annotation";
|
||||
import { Utils } from "../utils/index.utils";
|
||||
|
||||
/**
|
||||
* 视频播放的各种事件监听和处理
|
||||
*/
|
||||
export class EventListenersUi {
|
||||
|
||||
@Dom()
|
||||
private readonly Play!: HTMLElement;
|
||||
|
||||
@Dom()
|
||||
private readonly Video!: HTMLMediaElement;
|
||||
|
||||
@Dom()
|
||||
private readonly VideoProgress!: HTMLInputElement;
|
||||
|
||||
@Dom()
|
||||
private readonly VideoTime!: HTMLElement;
|
||||
|
||||
@Inject()
|
||||
private readonly Utils: Utils;
|
||||
|
||||
/**
|
||||
* 监听 视频数据长度发生改变的时候去获取视频的总时间,
|
||||
* 并设置到页面上显示出来
|
||||
* @constructor
|
||||
*/
|
||||
public BindEventDurationchange(): void {
|
||||
this.Video.addEventListener('durationchange', () => {
|
||||
this.setVideoTime(this.Video.duration)
|
||||
})
|
||||
}
|
||||
|
||||
// 播放按钮被点击,播放视频
|
||||
public BindEventEnded(): void {
|
||||
this.Video.addEventListener('ended', () => {
|
||||
// 视频播放完成后,将暂停按钮样式改为播放状态
|
||||
this.Play.classList.toggle(ClassName.Stop)
|
||||
// 回调用户的事件
|
||||
OptionParamsModel.OptionParams.finish(this.Video)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 视频播放过程中时间发生改变设置进度条进度
|
||||
* @constructor
|
||||
*/
|
||||
public BindEventVideoTimeUpdate() {
|
||||
this.Video.ontimeupdate = () => {
|
||||
// 进度条前进值 = 当前时间 / 总视频时间 * 100
|
||||
let w = this.Video.currentTime / this.Video.duration * 100;
|
||||
|
||||
// 设置进度条已播放过的颜色
|
||||
this.VideoProgress.style.backgroundSize = `${w}% 100%`;
|
||||
|
||||
// 设置进度条方块前进值
|
||||
this.VideoProgress.value = String(w)
|
||||
// 进度条步进值
|
||||
this.VideoProgress.step = '0.000000000000000001'
|
||||
this.setVideoTime()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 进度条被拖动的时候
|
||||
* @constructor
|
||||
*/
|
||||
public BindEventVideoOnProgress(): void {
|
||||
this.VideoProgress.oninput = () => {
|
||||
// 拿到被拖动滑块的滑动距离
|
||||
let offsetX = <any>this.VideoProgress.value;
|
||||
// 滑动后对应的视频播放时间 = 滑动距离 / 总距离 * 时间总时间
|
||||
this.Video.currentTime = offsetX / 100 * this.Video.duration;
|
||||
// 设置进度条已播放过的颜色
|
||||
this.VideoProgress.style.backgroundSize = `${offsetX}% 100%`;
|
||||
// 拖动的时候自动播放了,所以把播放按钮改成暂停按钮
|
||||
this.Play.classList.add(ClassName.Stop)
|
||||
this.setVideoTime()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置视频的 当前播放时间 / 视频总时间
|
||||
*/
|
||||
public setVideoTime(time?: number): void {
|
||||
if (time === undefined) {
|
||||
this.VideoTime.innerText = `${this.Utils.formatTime(this.Video.currentTime)}/${this.Utils.formatTime(this.Video.duration)}`;
|
||||
} else {
|
||||
this.VideoTime.innerText = `${this.Utils.formatTime(this.Video.currentTime)}/${this.Utils.formatTime(time)}`;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
38
src/core/ui/index.ui.ts
Normal file
38
src/core/ui/index.ui.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/// <reference path = "../../types/index.d.ts" />
|
||||
import OptionParamsModel from "../model/OptionParams.model";
|
||||
import { Utils } from "../utils/index.utils";
|
||||
import { Inject, Injectable } from "../annotation/Ioc.annotation";
|
||||
import { ControlsUi } from "./controls.ui";
|
||||
import { EventListenersUi } from "./eventListeners.ui";
|
||||
import { CoreSettingsUi } from "./coreSettings.ui";
|
||||
|
||||
/**
|
||||
* 生成播放器的Dom树,绑定各种事件处理
|
||||
*/
|
||||
@Injectable()
|
||||
export class ApplicationUI {
|
||||
|
||||
@Inject()
|
||||
private Utils: Utils;
|
||||
|
||||
|
||||
private el: HTMLElement
|
||||
|
||||
GenerateDom() {
|
||||
// 获取用户传入的最外层父节点
|
||||
this.el = this.Utils.$(OptionParamsModel.OptionParams.el);
|
||||
|
||||
// 保存视频播放器的DOM树,后面需要操作DOM树,为各个按钮绑定事件处理
|
||||
OptionParamsModel.VideoDom = this.Utils.ParseDom(this.Utils.getLayout());
|
||||
|
||||
// 自动调用传入类中方法名包含 'BindEventxxx' 的方法
|
||||
// 实现各种点击事件的自动绑定
|
||||
this.Utils.callMethods([
|
||||
CoreSettingsUi, ControlsUi, EventListenersUi
|
||||
])
|
||||
|
||||
// 将模板pug和参数配置渲染后得到html string,转换为dom树,
|
||||
// 然后将已经绑定好各种事件的dom注入到用户传入的 div 中给用户使用!!
|
||||
this.el.appendChild(OptionParamsModel.VideoDom);
|
||||
}
|
||||
}
|
||||
1
src/core/utils/.pydio
Normal file
1
src/core/utils/.pydio
Normal file
@@ -0,0 +1 @@
|
||||
b8fc9423-3e19-4e61-af70-8d7485eac391
|
||||
152
src/core/utils/index.utils.ts
Normal file
152
src/core/utils/index.utils.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import OptionParamsModel from "../model/OptionParams.model";
|
||||
import { Config } from "../config/index.config";
|
||||
import { Injectable } from "../annotation/Ioc.annotation";
|
||||
import * as Hls from "hls.js";
|
||||
import {VideoMimeType} from "../config/VideoMimeType.config";
|
||||
import {Dom} from "../annotation/Dom.annotation";
|
||||
@Injectable()
|
||||
export class Utils {
|
||||
|
||||
@Dom()
|
||||
private readonly Video!: HTMLVideoElement;
|
||||
|
||||
public $(el: string): any {
|
||||
return document.querySelector(el)
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动调用类中的事件绑定方法
|
||||
* @param params 类数组
|
||||
*/
|
||||
public callMethods(params: {new (...args: any[]): {}; }[] ): void {
|
||||
setTimeout(() =>
|
||||
params.forEach((val: { new (): any; } ) =>
|
||||
Object.getOwnPropertyNames(val.prototype).splice(1).filter(f =>
|
||||
f.includes("BindEvent")).forEach(v =>
|
||||
(new val())[v]())),400)
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改元素类名
|
||||
* @param dom
|
||||
* @param className
|
||||
*/
|
||||
addClassName<E extends HTMLElement>(dom: E, className: string): void {
|
||||
dom.classList.toggle(className)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 视同用户填入的模板,传入配置,渲染输出HTML
|
||||
*/
|
||||
public getLayout(): string {
|
||||
let render = require(`../../assets/tpl/${OptionParamsModel.OptionParams.layout}.pug`)
|
||||
return render({ params: Object.assign(OptionParamsModel.OptionParams, Config.IconConfig) })
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 string 转换为 dom 元素
|
||||
* @param arg
|
||||
* @constructor
|
||||
*/
|
||||
public ParseDom (arg: string): HTMLElement {
|
||||
var objE = document.createElement("div");
|
||||
objE.innerHTML = arg;
|
||||
return objE.childNodes[0] as HTMLElement;
|
||||
}
|
||||
|
||||
public Render() {
|
||||
|
||||
// // 获取dom树中最后一个child也就是 script
|
||||
// let oldScript = father.lastChild;
|
||||
// // 从dom树中删除 script 代码,因为不管是innerHTML还是appendChild dom中的script都不执行
|
||||
// // 所以需要特殊处理
|
||||
// father.removeChild(father.childNodes[father.childNodes.length-1])
|
||||
// // 将去除 script 后的dom树插入到网页中
|
||||
// el.appendChild(father);
|
||||
//
|
||||
// // 生成 script,写入 js,插入 到网页中,这时候js可以执行!
|
||||
// let newScript = document.createElement('script');
|
||||
// newScript.type = 'text/javascript';
|
||||
// newScript.innerHTML = (oldScript as HTMLElement).innerHTML;
|
||||
// el.appendChild(newScript);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用fetch 获取视频地址的编码类型,
|
||||
* 然后采用不同的播放方式
|
||||
* @constructor
|
||||
*/
|
||||
public async CheckVideoType(URL: string): Promise<void> {
|
||||
await fetch(`http://localhost:9000/check/url?url=${URL}`)
|
||||
.then((res)=>{
|
||||
return res.json()
|
||||
})
|
||||
.then((res)=>{
|
||||
switch (res.result) {
|
||||
// HLS 播放支持
|
||||
case VideoMimeType.HLS:
|
||||
this.PlayHls(this.Video, URL)
|
||||
break;
|
||||
// MP4播放支持
|
||||
case VideoMimeType.MP4:
|
||||
this.Video.src = URL;
|
||||
break;
|
||||
// MP4不同编码的播放支持
|
||||
case VideoMimeType.STREA:
|
||||
this.Video.src = URL;
|
||||
break;
|
||||
default:
|
||||
throw new Error("不支持播放的视频地址")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
* @param href 文件地址
|
||||
* @param fileType 文件类型
|
||||
*/
|
||||
downLoadFile(href: string, fileType = 'image/png') {
|
||||
var dlLink = document.createElement('a');
|
||||
dlLink.download = `AVideo_${(new Date()).valueOf().toString()}`;
|
||||
dlLink.href = href;
|
||||
dlLink.dataset.downloadurl = [fileType, dlLink.download, dlLink.href].join(':');
|
||||
|
||||
document.body.appendChild(dlLink);
|
||||
dlLink.click();
|
||||
document.body.removeChild(dlLink);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
* @param t
|
||||
*/
|
||||
formatTime(t: number){
|
||||
if (isNaN(t)) {
|
||||
return "00:00:00"
|
||||
}
|
||||
|
||||
var h: string | number = parseInt(String(t / 3600))
|
||||
h = h<10?'0'+h:h
|
||||
var m: string | number = parseInt(String(t % 3600 / 60))
|
||||
m = m<10?'0'+m:m
|
||||
var s: string | number = parseInt(String(t % 60))
|
||||
s = s<10?'0'+s:s
|
||||
return h+':'+m+':'+s
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用插件 hls.js 播放视频
|
||||
* @param Video
|
||||
* @param url
|
||||
* @constructor
|
||||
*/
|
||||
public PlayHls(Video: HTMLVideoElement, url: string) {
|
||||
let hls = new Hls();
|
||||
hls.loadSource(url);
|
||||
hls.attachMedia(Video);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user