EasyDSS高效能流媒體伺服器前端重構(四): webpack + video.js 打造流媒體伺服器前端
video.js 介紹
Video.js - open source HTML5 & Flash video player
作為一款高效能流媒體伺服器的前端, 必不可少會用到流媒體播放器. 在播放器的選擇上, 我們選中了功能強大並且開源的 video.js . 它可以用來播放 RTMP/HLS 直播流.
本篇介紹在 webpack 中整合 video.js 播放器元件, 我們將要完成一個 HLS 播放器
的小例子. 先來看一下效果圖吧:
安裝 video.js
我們要開發的 HLS 播放器 需要用到 video.js 的一個官方外掛: videojs-contrib-hls
儘管 video.js 官方文件中給出了 webpack 整合的說明(http://docs.videojs.com/tutorial-webpack.html), 但是在實際開發過程中, 我還是和其他人一樣遇到了很多坑(https://github.com/videojs/videojs-contrib-hls/issues/600) 最後, 算是將 video.js 整合好, 卻發現插放 HLS 流, 不能切換到 Flash 模式. 最終, 我決定採用外部依賴的方式整合 video.js, 正好藉此熟悉一下 webpack externals 的用法. 這裡介紹的也就是 “外部依賴法”.
既是”外部依賴法”, 那我們首先把外部依賴的 video.js 檔案準備好. 在 src 目錄下新建 externals 目錄, 把事先下載好的 video-js-5.19.2 目錄檔案拷貝到這裡.
修改 template.html 如下:
<html>
<head>
<title><%= htmlWebpackPlugin.options.title %></title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" name="viewport">
<!-- video.js -->
<link rel="stylesheet" href="/video-js-5.19.2/video-js.css"/>
<script src="/video-js-5.19.2/video.js"></script>
<script src="/video-js-5.19.2/videojs-contrib-hls4.js"></script>
</head>
<body class="skin-green sidebar-mini">
<div id="app"></div>
</body>
</html>
修改 webpack.dll.config.js 如下:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const webpack = require('webpack');
const path = require('path');
function resolve(dir) {
return path.resolve(__dirname, dir)
}
module.exports = {
entry: {
//提取共用元件, 打包成 vendor.js
vendor: ['jquery', 'vue', 'vuex', 'babel-polyfill',
'font-awesome/css/font-awesome.css', 'admin-lte/bootstrap/css/bootstrap.css',
'admin-lte/dist/css/AdminLTE.css', 'admin-lte/dist/css/skins/_all-skins.css',
'admin-lte/bootstrap/js/bootstrap.js', 'admin-lte/dist/js/app.js']
},
output: {
path: resolve('dll'),
filename: 'js/[name].[chunkhash:8].js',
library: '[name]_library'
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.common.js',
'jquery$': 'admin-lte/plugins/jQuery/jquery-2.2.3.min.js'
}
},
module: {
rules: [{
test: /\.css$/,
loader: 'style-loader!css-loader'
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader?limit=10000&name=images/[name].[hash:8].[ext]'
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader?limit=10000&name=fonts/[name].[hash:8].[ext]'
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader?limit=10000&name=media/[name].[hash:8].[ext]'
}]
},
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
"window.jQuery": 'jquery',
"window.$": 'jquery'
}),
new CleanWebpackPlugin(['dll']),
new CopyWebpackPlugin([
{ from: 'src/externals' }
]),
new webpack.DllPlugin({
path: resolve("dll/[name]-manifest.json"),
name: "[name]_library",
context: __dirname
}),
new HtmlWebpackPlugin({
filename: 'template.html',
title: '<%= htmlWebpackPlugin.options.title %>',
inject: 'head',
chunks: ['vendor'],
template: './src/template.html',
minify: {
removeComments: true,
collapseWhitespace: false
}
})
]
};
引入 CopyWebpackPlugin 將 externals 目錄下的外部依賴檔案拷貝到 dll 目錄, 最終, 這些外部依賴檔案將被拷貝到釋出目錄下
修改 webpack.config.js 如下:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const webpack = require('webpack');
const path = require('path');
require("babel-polyfill");
function resolve(dir) {
return path.resolve(__dirname, dir)
}
module.exports = {
//定義頁面的入口, 因為js中將要使用es6語法, 所以這裡需要依賴 babel 墊片
entry: {
index: ['babel-polyfill', './src/index.js'],
player: ['babel-polyfill', './src/player.js'],
about: ['babel-polyfill', './src/about.js']
},
output: {
path: resolve('dist'), // 指示釋出目錄
filename: 'js/[name].[chunkhash:8].js' //指示生成的頁面入口js檔案的目錄和檔名, 中間包含8位的hash值
},
externals: {
//video.js 作為外部資源引入
'video.js': 'videojs'
},
//下面給一些常用元件和目錄取別名, 方便在js中 import
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.common.js',
'jquery$': 'admin-lte/plugins/jQuery/jquery-2.2.3.min.js',
'src': resolve('src'),
'assets': resolve('src/assets'),
'components': resolve('src/components')
}
},
module: {
//配置 webpack 載入資源的規則
rules: [{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src')]
}, {
test: /\.vue$/,
loader: 'vue-loader'
}, {
test: /\.css$/,
loader: 'style-loader!css-loader'
},
{
test: /\.less$/,
loader: "less-loader"
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader?limit=10000&name=images/[name].[hash:8].[ext]'
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader?limit=10000&name=fonts/[name].[hash:8].[ext]'
},
{
test: /\.(swf|mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader?limit=10000&name=media/[name].[hash:8].[ext]'
}]
},
plugins: [
//引入全域性變數
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
"window.jQuery": 'jquery',
"window.$": 'jquery'
}),
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./dll/vendor-manifest.json')
}),
new CopyWebpackPlugin([
{ from: 'dll', ignore: ['template.html', 'vendor-manifest.json'] }
]),
//編譯前先清除 dist 釋出目錄
new CleanWebpackPlugin(['dist']),
//生成視訊廣場首頁, 在這個頁面中自動引用入口 index --> dist/js/index.[chunkhash:8].js
//以 src/index.html 這個檔案作為模板
new HtmlWebpackPlugin({
filename: 'index.html',
title: '視訊廣場',
inject: true, // head -> Cannot find element: #app
chunks: ['index'],
template: './dll/template.html',
minify: {
removeComments: true,
collapseWhitespace: false
}
}),
new HtmlWebpackPlugin({
filename: 'player.html',
title: 'HLS 播放器',
inject: true,
chunks: ['player'],
template: './dll/template.html',
minify: {
removeComments: true,
collapseWhitespace: false
}
}),
//生成版本資訊頁面, 在這個頁面中自動引用入口 about --> dist/js/about.[chunkhash:8].js
//以 src/index.html 這個檔案作為模板
new HtmlWebpackPlugin({
filename: 'about.html',
title: '版本資訊',
inject: true,
chunks: ['about'],
template: './dll/template.html',
minify: {
removeComments: true,
collapseWhitespace: false
}
})
]
};
重點是在 externals 塊下面宣告 videojs 作為外部資源使用
然後, 我們新增一個新的靜態頁面配置, 用做 HLS 播放器的入口
新增左側選單項
開啟 src/store/index.js, 修改如下:
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
logoText: "EasyDSS",
logoMiniText: "DSS",
menus: [
{
path: "/index.html",
icon: "mouse-pointer",
text: "視訊廣場"
}, {
path: "/player.html",
icon: "play",
text: "HLS 播放器"
}, {
path: "/about.html",
icon: "support",
text: "版本資訊"
}
]
},
getters : {
},
mutations: {
},
actions : {
}
})
export default store;
編寫HLS 播放器 頁面
將 video.js 簡單封裝成元件, 新建 src/compontents/VideoJS.vue
<template>
<div class="player-wrapper">
<div class="video-wrapper" style="padding-bottom:55%;position:relative;margin:0 auto;overflow:hidden;">
<div class="video-inner" style="position:absolute;top:0;bottom:0;left:0;right:0;">
</div>
</div>
</div>
</template>
<script>
videojs.options.flash.swf = '/video-js-5.19.2/video-js-fixed.swf';
videojs.options.techOrder = ['html5', 'flash'];
if (videojs.browser.IE_VERSION) { // if IE use flash first
videojs.options.techOrder = ['flash', 'html5'];
}
export default {
data() {
return {
player: null
}
},
props: {
videoUrl: {
default: ""
},
autoplay: {
default: true
}
},
beforeDestroy() {
this.destroyVideoJS();
},
deactivated() {
this.destroyVideoJS();
},
watch: {
videoUrl: function(val) {
this.destroyVideoJS();
this.initVideoJS();
}
},
mounted() {
this.initVideoJS();
},
computed: {
type() {
let _type = "application/x-mpegURL";
if (this.rtmp) {
_type = "rtmp/mp4";
}
return _type;
},
rtmp() {
return (this.src || "").indexOf("rtmp") == 0;
},
src() {
if (!this.videoUrl) {
return "";
}
if (this.videoUrl.indexOf("/") === 0) {
return location.protocol + "//" + location.host + this.videoUrl;
}
return this.videoUrl;
},
videoHtml() {
return `
<video class="video-js vjs-default-skin vjs-big-play-centered" style="width: 100%; height: 100%;" controls preload="none">
<source src="${this.src}" type="${this.type}"></source>
<p class="vjs-no-js">
To view this video please enable JavaScript, and consider upgrading to a web browser that
<a href="http://videojs.com/html5-video-support/" target="_blank">
supports HTML5 video
</a>
</p>
</video>
`;
}
},
methods: {
destroyVideoJS() {
if (this.player) {
this.player.dispose();
this.player = null;
}
},
initVideoJS() {
$(this.$el).find(".video-inner").empty().append(this.videoHtml);
if (!this.src) {
return;
}
if (this.rtmp) {
this.player = videojs($(this.$el).find("video")[0], {
notSupportedMessage: '您的瀏覽器沒有安裝或開啟Flash',
tech: ['flash'],
autoplay: this.autoplay
});
this.player.on("error", e => {
var $e = $(this.$el).find(".vjs-error .vjs-error-display .vjs-modal-dialog-content");
var $a = $("<a href='http://www.adobe.com/go/getflashplayer' target='_blank'></a>").text($e.text());
$e.empty().append($a);
})
} else {
this.player = videojs($(this.$el).find("video")[0], {
autoplay: this.autoplay
});
}
}
}
}
</script>
封裝 video.js api
編寫播放器彈出框元件, 新建 src/components/VideoDlg.vue
<template>
<div class="modal fade" data-keyboard="false" data-backdrop="static">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<h4 class="modal-title text-success text-center">{{videoTitle}}</h4>
</div>
<div class="modal-body">
<VideoJS v-if="bShow" :videoUrl="videoUrl"></VideoJS>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">關閉</button>
</div>
</div>
</div>
</div>
</template>
<script>
import VideoJS from './VideoJS.vue'
export default {
data() {
return {
videoUrl: "",
videoTitle: "",
bShow: false
}
},
mounted() {
$(document).on("hide.bs.modal", this.$el, () => {
this.bShow = false;
}).on("show.bs.modal", this.$el, () => {
this.bShow = true;
})
},
components: { VideoJS },
methods: {
play(src,title) {
this.videoUrl = src||"";
this.videoTitle = title||"";
$(this.$el).modal("show");
}
}
}
</script>
封裝 bootstrap 模態框
編寫HLS播放器頁面內容, 新建 src/components/Player.vue
<template>
<div class="container-fluid no-padding">
<br>
<div class="col-sm-8 col-sm-offset-2">
<form role="form" class="form-horizontal" id="url-form">
<div class="form-group">
<div class="input-group" id="input-url-group">
<input type="text" class="form-control" id="input-url" name="url" placeholder="輸入播放地址" v-model.trim="url" @keydown.enter.prevent="play">
<span class="input-group-btn">
<a class="btn btn-primary" role="button" @click.prevent="play">
<i class="fa fa-play"></i> 播放</a>
</span>
</div>
</div>
</form>
</div>
</div>
</template>
<script>
import Vue from 'vue'
import { Message } from 'element-ui'
Vue.prototype.$message = Message;
export default {
data() {
return {
url: ""
}
},
methods: {
play() {
if (!this.url) {
this.$message({
type: 'error',
message: "播放地址不能為空"
});
return;
}
this.$emit("play", { videoUrl: this.url, videoTitle: this.url});
}
}
}
</script>
這裡順帶演示了 element-ui 的 Message 用法
點選播放按鈕, 訊息向父元件傳遞, 播放地址作為引數一起傳遞
編寫入口 js , 新建 src/player.js
import Vue from 'vue'
import store from "./store";
import AdminLTE from './components/AdminLTE.vue'
import Player from './components/Player.vue'
import VideoDlg from './components/VideoDlg.vue'
new Vue({
el: '#app',
store,
template: `
<AdminLTE>
<VideoDlg ref="videoDlg"></VideoDlg>
<Player @play="play"></Player>
</AdminLTE>`,
components: {
AdminLTE, Player, VideoDlg
},
methods: {
play(video){
this.$refs.videoDlg.play(video.videoUrl, video.videoTitle);
}
}
})
接收 Player 元件傳來的播放訊息, 開啟播放器彈出框, 完成視訊播放
執行
我們修改了 template.html 和 webpack.dll.config.js , 所以先要重新 build 共用元件庫
npm run dll
然後
npm run start