java采用WebSocket向前端传输rtp封装的h264码流,使用wfs.js接收h264流,并在前端页面播放

使用:监听到项目启动后就开启Udpserver,然后再网页上选择下拉列表,开始播放(需要有一个udp一直发送rtp包,收到了之后通过WebSocket发送给前端即可)。
gitee下载

Maven依赖

导入webSocket的支持jar包

    <dependency>
      <groupId>javax</groupId>
      <artifactId>javaee-api</artifactId>
      <version>7.0</version>
      <scope>provided</scope>
    </dependency>

WebSocket后端代码

这里我写的是当请求视频时,前端才会与后端建立长连接,然后后端开始推送视频流送给前端解析。里面用HashMap存储每个session对象,并有唯一标识,方便发送视频流时调用。这里借鉴了后端的websocket入门代码。先用文件测试,能够发送成功之后开始移植发送rtp封装h264的视频流。

import java.io.*;
import java.nio.ByteBuffer;
import java.util.*;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

/**
 * @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,
 *                 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
 */
@ServerEndpoint(value = "/{devid}")//{}中的数据代表一个参数,多个参数用/分隔
public class WebSocketTest {
    // 用来存放每个客户端对应的WebSocket对象
    public static final HashMap<String, WebSocketTest> dev_webSocket = new HashMap<>();
    // 与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;
    /**
     * 连接建立成功调用的方法
     *
     * @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    @OnOpen
    public void onOpen(@PathParam(value = "devid") String devid, Session session) {
        this.session = session;
        dev_webSocket.put(devid, this); // 加入map中
        System.out.println("有连接接入" + devid);
//        sendMessage(new File("xxx.h264"));先用文件测试是否能播放
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(@PathParam(value = "devid") String devid) {
        dev_webSocket.remove(devid); // 从map中删除
        System.out.println(devid + "连接关闭");
        timer.cancel();
        try {
            session.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 发生错误时调用
     */
    @OnError
    public void onError(Session session, Throwable error) {
        System.out.println("发生错误" + error.getMessage());
    }

    /**
     * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。
     */
    public void sendMessage(String message) {
        try {
            this.session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void sendMessage(byte[] video) {
        System.out.println("本帧数据长度为"+video.length);
        try {
            ByteBuffer bf = ByteBuffer.wrap(video);
            this.session.getBasicRemote().sendBinary(bf);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 这里以sps / pps / 帧 分成三段发送
     * 第一次sps 第二次 pps 第三次就是数据(分片需要拼成整体之后再送)周而复始
     * @param file
     */
    public void sendMessage(File file) {
        FileInputStream fileInputStream=null;
        try {
            fileInputStream= new FileInputStream(file);
            int len=fileInputStream.available();
            byte[] tmp=new byte[len];
            byte[] frame;
            ByteBuffer byteBuffer;
            //找到帧
            int front=0;
            int read = fileInputStream.read(tmp);
            System.out.println("读完为-1"+read);
            for (int i=0;i< len;i++){
                if (i+3>len){
                    return;
                }
                if (tmp[i]==0&&tmp[i+1]==0&&tmp[i+2]==0&&tmp[i+3]==1){//非一次开头
                    if (i-4<0){
                        continue;
                    }
                    frame=new byte[i-front];
                    System.arraycopy(tmp,front,frame,0,frame.length);
                    byteBuffer=ByteBuffer.wrap(frame);
                    this.session.getBasicRemote().sendBinary(byteBuffer);
//                    System.out.println("发送数据帧长度"+Arrays.toString(frame));
                    front=i;
                }
            }
        } catch (IOException e) {
            System.out.println("用户退出网页");
        } finally {
            if (fileInputStream!=null){
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Udp接收rtp数据

开一个Udp的服务器,接收设备发送的rtp包,接收到之后,根据用户请求的唯一标识,将处理rtp包装的h264重新组包之后送给前端。标识现在默认为admin

package com;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;


public class UdpReceiveData extends Thread{
    private DatagramSocket server;
    public static ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(20,40,30,TimeUnit.SECONDS,
            new ArrayBlockingQueue<Runnable>(10),new ThreadPoolExecutor.DiscardPolicy());
    public UdpReceiveData(int rtpPort) {
        try {
            this.server=new DatagramSocket(rtpPort);
        } catch (SocketException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        openRtp();
    }
    public void startRun(){
        threadPoolExecutor.execute(this);
    }
    /**
     * RTP的接收UDP  一般情况PT=96 为H264
     */
    public void openRtp() {
        System.out.println("**********************rtp开始接收数据**********************");
        DatagramPacket client;
        //一次接收数据的字节数组大小
        byte[] bytes=new byte[1500];
        RtpDataDeal rtpDataDeal=new RtpDataDeal();
        try {
            //开始收数据
            while (true) {
                //一次最大能接受2M 且每次需要重新定义否则,下一次的数据长度没这次长时,会将这次的数据填补
                client=new DatagramPacket(bytes, bytes.length);
                server.receive(client);
                //本次传输数据的长度
                WebSocketTest admin = WebSocketTest.dev_webSocket.get("admin");
                if (admin!=null){
                    byte[] bytes1 = rtpDataDeal.convergeBytes(client.getData(), client.getLength());
                    if (bytes1!=null){
                        admin.sendMessage(bytes1);
                    }
                }
            }
        } catch (IOException ignored) {
        }finally {//关闭
            server.close();
        }
    }
}

处理组装rtp携带h264类

去掉rtp头后,不分片的直接在前面加上0001,分片的头部除了要去掉rtp头外,还要再减去rtp头部后两个字节,然后加上0001和rtp头部后两个字节的合并的一个字节,分片的其余部分直接去掉rtp头部和rtp头部的后两个字节,然后装在前面部分的后面即可。

package com;


import java.util.ArrayList;
import java.util.ListIterator;

public class RtpDataDeal extends Thread{
    //单个文件的标志
    private boolean singleFlag = false;
    //分片时nal的头字节由FU indicator的前三位和FU Header的后五位组成
    private byte dataHead;
    private final byte[] pierce = new byte[]{0, 0, 0, 1};//固定分割0001
    private final byte[] pierceAndHead=new byte[]{0,0,0,1,-1};//固定分割0001+两个字节组合成的头
    private ArrayList<byte[]> arrayList=new ArrayList<>(128);
    private int pierceLength=0;
    //分片的起始标志
    private boolean startFlag;
    private boolean endFlag;
    //rtp头部长度,也是除开rtp头后第一个字节的下标
    private static final int rtpHeadLength=12;
    /**
     * 解包时,取FU indicator的前三位和FU Header的后五位构成一个字节
     * 功能:分析处理rtp中h.264的的indicator和header
     *
     * @param fu1 头部的后两个字节(1)
     * @param fu2 头部的后两个字节(2)
     */
    public void dealNalData(byte fu1, byte fu2) {
        //FU indicator
        //f 0表示正常,128表示错误
        int f = fu1 & 0x80;//128   0
        //NRI 重要级别,11表示非常重要
        int nri = fu1 & 0x60;
        //FU Type 表示该NALU的类型是什么 28表示FU-A分片单元 1代表不分区 7代表SPS 8代表PPS 5代表IDR
        int fu_type = fu1 & 0x1f;//为28代表分包
        //FU Header
        //起始帧,为1表示分片的第一包
        //分包的起始包
        startFlag = (128 == (fu2 & 0x80));
        //末尾帧,为1表示分片的最后一包
        //结束包
        endFlag = (64 == (fu2 & 0x40));//前面表示要分片
        //表示不分片,一次单个NAL单元
        singleFlag = fu_type <= 23 && fu_type >= 1;
        //nal类型,表示为 什么帧
        int nalType = fu2 & 0x1f;
        if (fu_type == 28) {//分包时需将FU indicator的前三位和FU Header的后五位为1个字节放入
            dataHead = (byte) (f + nri + nalType);
        }
    }


    /**
     * 功能:拼接头部的4个字节 0 0 0 1 以及 nal 和 本次的荷载数据
     * @param rtpBody 本次的数据数组去掉rtp头之后的数据
     *  返还拼接了0 0 0 1 、 nal 和 荷载数据的数组
     */
    public byte[] convergeBytes(byte[] rtpBody,int length){
        dealNalData(rtpBody[rtpHeadLength],rtpBody[rtpHeadLength+1]);//处理此次的头两字节信息
        if (singleFlag) {//不分片
            //在头部加上0 0 0 1四个字节
            byte[] newByte=new byte[4+length-12];
            System.arraycopy(pierce,0,newByte,0,4);
            System.arraycopy(rtpBody,12,newByte,4,length-12);
            return newByte;
        }else if (startFlag){//要分片,分片的第一片才加,其余不加
            //+4 是加 0 0 0 1四个字节 +1 是加合并的头  -2 是减去头部的两个字节(因为这两个要合并成一个) +4 +1 -2
            byte[] newByte=new byte[3+length-12];
            pierceAndHead[4]=dataHead;
            System.arraycopy(pierceAndHead,0,newByte,0,5);
            //在头部加上0 0 0 1四个字节 和 dataHead 取indicator前3 和 head后5
            System.arraycopy(rtpBody,14,newByte,5,length-14);
            arrayList.add(newByte);
            pierceLength+=newByte.length;
            //这里因为rtpBody是整个数据包括前两个需要合并的字节,所以需要从rtpBody的第三个下标也就是2开始复制
            //因为从第三个字节开始复制,长度也需要减去2,因为不减长度的话没这么多位
            return null;
        }else {
            byte[] newByte=new byte[length-12-2];
            System.arraycopy(rtpBody,14,newByte,0,length-14);
            arrayList.add(newByte);
            pierceLength+=newByte.length;
            if (endFlag){
                return pinJie();
            }else return null;
        }
    }
    private byte[] pinJie(){
        byte[] needSend=new byte[pierceLength];
        ListIterator<byte[]> listIterator = arrayList.listIterator();
        int index=0;
        while (listIterator.hasNext()){
            byte[] next = listIterator.next();
            System.arraycopy(next,0,needSend,index,next.length);
            index+= next.length;
        }
        arrayList=new ArrayList<>(128);
        pierceLength=0;
        return needSend;
    }
}

前端代码

<!DOCTYPE html>
<html lang="en">
	<head>
		<title>播放h264</title>
		<meta charset="utf-8">
		<script type="text/javascript" src="wfs.js"></script>
	</head>
	<body>
		<select id="sel" name="test">
			<option value="请选择">请选择</option>
			<option value="admin">admin</option>
		</select>
		<h2>播放h264</h2>
		<div class="wfsjs">
			<video muted id="video1" width="640" height="480" controls autoplay></video>
			<div class="ratio"></div>
		</div>
		<script src="jquery-3.4.1.js" type="text/javascript"></script>
		<script type="text/javascript">
			$(function () {
					$("#sel").on("change",function () {
						if ($("#sel").val()!="请选择"){
							if (Wfs.isSupported()) {
								var video1 = document.getElementById("video1"),wfss = new Wfs();
								// wfss.attachMedia(video1,'ch1',"H264Raw",$("#sel").val());
								wfss.attachMedia(video1,'ch1',"H264Raw","admin");//第四个即为设备标识,有需求可改为动态的
							}
						}
					})
			})
		</script>
	</body>
</html>

引入的js文件

jquery-3.4.1.js
wfs.js(var client = new WebSocket(‘ws://’ + ‘localhost:8080’ + ‘/web/’+data.websocketName);localhost:8080/web为我tomcat的url,如果需要放到公网,需要修改localhost为公网ip)

待解决问题

经测试,功能基本实现,但存在以下问题:

  1. 无法自动播放,需要人为的点一下播放按钮,在火狐上,还需拖动一下进度条。
  2. 由于调用的wfs.js,不能直接使用close关闭websocket,刷新页面能关掉。
  3. 视频乱序,采用的udp传输有一定的乱序,目前我想到的方法就是,存一定的视频帧之后就开始排序,然后将前面的输出,缺失的就直接丢弃,借用了一点Tcp中滑动窗口的模型。
  • 5
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 23
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

syf_wfl

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值