H5播放H264之websocket

码农天地 -
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 >
        <video id="video1" width="640" height="480" controls></video>
        <div ></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

特别申明:本文内容来源网络,版权归原作者所有,如有侵权请立即与我们联系(cy198701067573@163.com),我们将及时处理。

Tags 标签

加个好友,技术交流

1628738909466805.jpg