zoukankan      html  css  js  c++  java
  • 高性能流媒体服务器EasyDSS前端重构(四)- webpack + video.js 打造流媒体服务器前端

    接上篇

    接上篇《高性能流媒体服务器EasyDSS前端重构(三)- webpack + vue + AdminLTE 多页面引入 element-ui

    本文围绕着实现EasyDSS高性能流媒体服务器的前端框架来展开的,具体EasyDSS的相关信息可在:www.easydss.com 找到!

    video.js 介绍

    Video.js - open source HTML5 & Flash video player

    作为一款高性能流媒体服务器的前端, 必不可少会用到流媒体播放器. 在播放器的选择上, 我们选中了功能强大并且开源的 video.js . 它可以用来播放 RTMP/HLS 直播流.

    本篇介绍在 webpack 中集成 video.js 播放器组件, 我们将要完成一个 HLS 播放器 的小例子. 先来看一下效果图吧:

    HLS 播放器一

    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=" 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">&times;</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

    源码位置: https://github.com/penggy/easydss-web-src/tree/blog_4

    获取更多信息

    邮件:support@easydarwin.org

    WEB:www.EasyDarwin.org

    Copyright © EasyDarwin.org 2012-2017

    EasyDarwin

  • 相关阅读:
    「BZOJ2721」「LuoguP1445」 [Violet]樱花(数论
    「USACO08DEC」「LuoguP2921」在农场万圣节Trick or Treat on the Farm(tarjan
    「HNOI2008」「LuoguP3197」越狱(数论
    「CF779B」「LOJ#10201.」「一本通 6.2 练习 4」Sherlock and His Girlfriend(埃氏筛
    「LOJ#10072」「一本通 3.2 例 1」Sightseeing Trip(无向图最小环问题)(Floyd
    「LOJ#10068」「一本通 3.1 练习 3」秘密的牛奶运输(次小生成树
    「USACO15FEB」「LuoguP3121」审查(黄金)Censoring (Gold)(AC自动机
    「LOJ#10056」「一本通 2.3 练习 5」The XOR-longest Path (Trie
    「LOJ#10051」「一本通 2.3 例 3」Nikitosh 和异或(Trie
    「UVA644」 Immediate Decodability(Trie
  • 原文地址:https://www.cnblogs.com/babosa/p/7468280.html
Copyright © 2011-2022 走看看