乐趣区

关于视频处理:H5播放H264之websocket

一、简介

对于前端应用 websocket 播放音视频我倒是没想过,然而实践上的确是可行的,因为 websocket 是长连贯,尽管晓得 web 端的常见用法,然而作为 c ++ 开发人员我最纳闷的问题就是:

  • 应用 js 解决二进制?这个做法很不常见,恕我没太多理解,我始终认为 js 个别解决二进制不不便,所以脑海里始终自以为是
  • js 编解码效率高吗?因为是基于浏览器的脚本语言的二不是间接基于零碎 api 的独立进行,转行较多,效率应该不高,所以我也始终避讳用 js 去解决编解码

其实,我自认为的通过一直的摸索发现也的确是对的,相对而言 js 的确可能解决二进制流(应用 ArrayBuffer)然而操作不不便且效率绝对较低,不过对于 web 端播放来讲,第一是播放量不太,个别不会呈现 9 路以上主码流播放的状况(个别是一路并发),第二就是 web 端播放编解码其实并不是像通过 c /C++ 那样将所有的数据一步一步解码进去,而是能够借助 EMS 转换工具将 C 库间接转换为 js 脚本,咱们只须要用 c 或 c ++ 语言进行开发即可!

二、c/C++ 转 js

我这里多说一句,我看到有将 c 语言间接转换为 js 语言的库的时候,我是一愣,这不说来所谓的前端 js 的音视频开发的开源库那么多,都能够应用 c 语言间接开发进去?因为开源的有如下问题:

  • 有些局部开源,不收费
  • 有些有厌恶的 logo,无奈定制
  • 有些有 bug,并不欠缺
  • 有些没有 bug 然而无奈定制(比方提早较高、播放长时间会变慢等疑难杂症)

如果可能用 c 或 cpp 编程开发,对于相熟善于这块的我岂不乐哉!于是我搜寻了一下 c 或 cpp 转 js 的库

  • ecmascripten

Emscripten,基于 LLVM 可将 C /C++ 代码编译为 js 的工具。

  • Asm.js

Asm.js 来自于 JavaScript 利用的一个新畛域:编译成 JavaScript 的 C /C++ 利用。它是 JavaScript 利用的一个全新流派,由 Mozilla 的 Emscripten 我的项目催生而来。

Emscripten 我的项目提供了能够编译 C 和 C ++(或其余任何可转换为 LLVM IR 的语言)代码为 asm.js 的工具,如下 c 语言代码:

int (int i)
{return i + 1;}

Emscripten 将输入下列 JavaScript 代码:

function (i)
{
    i = i | 0; 
    return (i + 1) | 0;
}

Emscripten 目标就是将 c /c++ 过程编译成 js 或者 H5 利用,asm.js 的产生是为了进步 Emscripten 转换后的代码执行效率的。流程是 C ++ -> Emscripten -> asm.js -> 浏览器运行。

咱们晓得 js 的性能是无奈跟 C ++ 这种高效语言相比的,然而 Asm.js 比拟留神性能优化,个别状况下,对于简单的利用 Asm.js 的性能仅仅比一般 C ++ 编译的慢两倍(能够和 Java 或者 C# 相媲美)。

Asm.js 为了优化性能,做了一下几点:

  • 所有内部数据在一个称为堆的对象中存储并被援用。堆在实质上是一个大数组(该当是一个在性能上高度优化的类型化数组)。所有的数据在这个数组中存储——无效的代替了全局变量,构造体,闭包和其余模式的数据存储。
  • 能解决被挑出的几种不同的数值类型,而没有提供其余的数据类型(包含字符串,布尔型和对象)。
  • 当拜访和赋值变量时,后果被对立的强制转换成一种特定类型。例如 f = e | 0; 给变量 f 赋值 e,但它也确保了后果的类型是一个整数(| 0 把值转换成整数,确保了这点)。

通过以上几点,能够看进去尽管 js 语言是一门动静语言,在过程运行时变量的类型是不确定的,然而 Asm.js 没有这个问题,他确保过程运行时变量类型已知(能够转换),让 js 实现了动态语言的概念。
同时,在内存操作上,将变量寄存在一大块内存上,相当于在栈上操作(实际上是堆)。

三、websocket 播放 H264 流

咱们晓得了以上概念之后,咱们前端播放视频裸流用的一个库及时 wfs,它曾经帮咱们应用了通过 websocket 接管二进制 H264 数据并解码渲染的性能,这里咱们就不必过多的去操心了,站在伟人的肩膀上再翻新!

通过 websocket 先天编程,减少 h264 文件读取发送的性能终于出了成果,而且十分的晦涩。

前端播放完结 wfs 的代码如下:

<!DOCTYPE html>
<html>
<head>
    <title>h.264 播放 </title>
    <meta charset="utf-8">
    <script type="text/javascript" src="wfs.js"></script>
</head>
<body>
    <h2> 播放 H264 裸流 (h.264 转 fmp4)</h2>
    <div class="wfsjs">
        <video id="video1" width="640" height="480" controls></video>
        <div class="ratio"></div>
    </div>
    <button onclick="clickbtn()"> 开始播放 </button>
    
    <script type="text/javascript">
        function clickbtn() {if (Wfs.isSupported()) {
                // 创立 WFS 库
                wfs = new Wfs();
                // 获取元素
                var video1 = document.getElementById("video1");
                // 关联到通道 ch1
                wfs.attachMedia(video1, 'ch1');
            }
        };
    </script>
</body>
</html>

后端 websocket 读取文件外围代码如下:

package com.easystudy.websocket;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import org.springframework.stereotype.Component;

import com.easystudy.service.H264Reader;
import com.easystudy.service.H264Reader.Frame;
import com.easystudy.util.PoolHelper;

/**
 * @ServerEndpoint 该注解能够将类定义成一个 WebSocket 服务器端,* @OnOpen         示意有浏览器链接过去的时候被调用
 * @OnClose     示意浏览器收回敞开申请的时候被调用
 * @OnMessage     示意浏览器发消息的时候被调用
 * @OnError     示意报错了
 * @欢送退出群聊, 一起分享, 一起单干, 一起提高
 * QQ 交换群:961179337
 * 微信账号:lixiang6153
 * 微信公众号:IT 技术快餐
 * 电子邮箱:lixx2048@163.com
 */
@Component
@ServerEndpoint("/play2")
public class MessageEndPoint extends BaseWS{private H264Reader h264Reader = new H264Reader();
    
    // concurrent 包下线程平安的 Set
    private static final CopyOnWriteArraySet<MessageEndPoint> SESSIONS = new CopyOnWriteArraySet<>();
    // 以后连贯会话信息
    private Session session;
    // 是否进行发送音视频
    private boolean stop = false;

    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        SESSIONS.add(this);
        System.out.println(String.format("胜利建设连贯~ 以后总连接数为:%s", SESSIONS.size()));
        
        stop = false;
        h264Reader.open("test.h264");
        PoolHelper.execute(new Runnable() {
            @Override
            public void run() {System.out.println("开始发送");
                byte[] szStartCode = {0x00, 0x00, 0x00, 0x01};
                while (!stop) {Frame frame = h264Reader.readFrame(szStartCode);
                    if (null == frame || frame.getLength() <= 0) {break;}
                    sendMessage(frame.getData());
                    System.out.println("发送数据帧:" + frame.getLength());
                    try {// 这里模仿须要思考读取文件的工夫所以不按 40ms(25fps), 否则导致卡顿
                        // Thread.sleep(40);
                        Thread.sleep(30);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                }
                if (!stop) {
                    try {byte[] b = new byte[0];
                        sendMessage(b, true);
                        session.close();} catch (IOException e) {e.printStackTrace();
                    }
                }
                System.out.println("完结发送");
            }
        });
    }
    
    @OnClose
    public void onClose() {SESSIONS.remove(this);
        System.out.println(String.format("胜利敞开连贯~ 以后总连接数为:%s", SESSIONS.size()));
        
        stop = true;
        h264Reader.close();}
    
    @OnMessage
    public void onMessage(String message, Session session) {System.out.println("收到客户端【" +session.getId()+ "】音讯:" + message);
    }
    
    @OnError
    public void onError(Session session, Throwable error) {System.out.println("产生谬误");
        error.printStackTrace();}
    
    /**
     * 指定用户发文本音讯
     * @param message
     */
    public void sendMessage(String message) {
        try {this.session.getBasicRemote().sendText(message);
        } catch (IOException e) {e.printStackTrace();
        }
    }
    
    /**
     * 指定用户发二进制音讯并指定标记
     * @param message
     */
    public void sendMessage(byte[] message, boolean end) {
        try {ByteBuffer bf = ByteBuffer.wrap(message);
            this.session.getBasicRemote().sendBinary(bf, end);
        } catch (IOException e) {e.printStackTrace();
        }
    }
    
    /**
     * @性能形容: 发送二进制数据 - 无完结标记
     * @版权信息:www.easystudy.com
     * @编写作者:lixx2048@163.com
     * @开发日期:2020 年 9 月 21 日
     * @备注信息:*/
    public void sendMessage(byte[] message) {
        try {ByteBuffer bf = ByteBuffer.wrap(message);
            this.session.getBasicRemote().sendBinary(bf);
        } catch (IOException e) {e.printStackTrace();
        }
    }
    
    /**
     * @性能形容: 群发音讯: 静态方法
     * @版权信息:www.easystudy.com
     * @编写作者:lixx2048@163.com
     * @开发日期:2020 年 9 月 21 日
     * @备注信息:*/
    public static void fanoutMessage(String message) {SESSIONS.forEach(ws -> ws.sendMessage(message));
    }
}

这里的外围就是当 websocket 连贯上来的时候,通过读取一帧 H264 数据而后发送给前端,默认帧率 25 帧,所以发送一帧的时候须要睡眠 40ms,然而这里如果间接睡眠 40ms,前端可能播放渲染不晦涩,起因是因为读取 H264 文件的时候也须要工夫的,所以我这里睡眠了 30ms。

后盾发送数据截图:

另外这块须要留神的是 wfs.js 库:

  • wfs 库中连贯的地址是 ws://localhost:8080/, 如果地址不对须要批改 wfs.js 的地址
    key: 'onMediaAttached',
    value: function onMediaAttached(data) {if (data.websocketName != undefined) {var client = new WebSocket('ws://' + '127.0.0.1:8080' + '/' + data.websocketName);
        this.wfs.attachWebsocket(client, data.channelName);
        console.log('websocket connect');
      } else {console.log('websocketName ERROE!!!');
      }
    }

这里能够依据须要批改域名或增加本人的项目名称定制。

  • 后盾我的项目不须要我的项目上下文名称(也就是项目名称),websocket 否则连贯不上

通过测试 google 和 360 浏览器都能够间接播放 H264 数据,而不须要额定的插件或平安设置,堪称完满!

源码获取、单干、技术交换请获取如下联系方式:
QQ 交换群:961179337

微信账号:lixiang6153
公众号:IT 技术快餐
电子邮箱:lixx2048@163.com

退出移动版