Skip to content

使用Nodejs将h264/h265转码成mp4或某一帧的图片

前置知识

  • 认识FFmpeg

    1. FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。有非常强大的功能包括视频采集功能、视频格式转换、视频抓图、给视频加水印等。
    2. 官网 http://ffmpeg.org/
    3. 相关指令集学习 http://ffmpeg.org/ffmpeg-filters.html
  • 安装ffmpeg

    1. Linux
      work@pavaro-cloud.bcc-szth:~$ yum install -y ffmpeg
      # 此步骤安装失败参考以下步骤
      
      • centos6.x或centos7.x安装ffmpeg方法
        1. 下载ffmpeg源码包
          work@pavaro-cloud.bcc-szth:~$ wget http://www.ffmpeg.org/releases/ffmpeg-3.1.tar.gz
          
        2. 解压ffmpeg源码包并编译
          1
          2
          3
          work@pavaro-cloud.bcc-szth:~$ tar -zxvf ffmpeg-3.1.tar.gz
          work@pavaro-cloud.bcc-szth:~$ cd ffmpeg-3.1
          work@pavaro-cloud.bcc-szth:~$ ./configure --prefix=/usr/local/ffmpeg
          
        3. 执行安装
          work@pavaro-cloud.bcc-szth:~$ make && make install
          
        4. 在系统目录下使用vi命令把Ffmpeg加入全局变量,使之所有都可以使用ffmpeg调用
          work@pavaro-cloud.bcc-szth:~$ vi /etc/profile
          
        5. 在文件最后PATH添加环境变量
          work@pavaro-cloud.bcc-szth:~$ export PATH=/usr/local/ffmpeg/bin/:$PATH
          
        6. 重载profile
          work@pavaro-cloud.bcc-szth:~$ source /etc/profile
          
        7. 测试变量和安装是否完成,任何地方使用ffmpeg -version 查看版本即可
          work@pavaro-cloud.bcc-szth:~$ ffmpeg -version
          ffmpeg version 3.1 Copyright (c) 2000-2016 the FFmpeg developers
          built with gcc 4.4.6 (GCC) 20120305 (Red Hat 4.4.6-4)
          configuration: --prefix=/usr/local/ffmpeg
          libavutil      55. 27.100 / 55. 27.100
          libavcodec     57. 48.101 / 57. 48.101
          libavformat    57. 40.101 / 57. 40.101
          libavdevice    57.  0.101 / 57.  0.101
          libavfilter     6. 46.102 /  6. 46.102
          libswscale      4.  1.100 /  4.  1.100
          libswresample   2.  1.100 /  2.  1.100
          
    2. Mac
  • 指令范例

    1. 将h264转换为mp4
      ffmpeg -i test.h264 -c:v libx264 -strict -2 test.mp4
      
    2. 将mp4按指定帧率切割成图片
      ffmpeg -i test.mp4 -r 指定帧率 -f image2 test/image-%03d.jpg
      
    3. 对于24帧每秒的视频 128帧是第5秒的第四帧
      ffmpeg -i test.h264 -threads 1 -ss 00:00:05.167 -f image2 -r 1 -t 1 -s 256*256 test-%2d.jpg
      
    4. 要在1秒到20秒允许一个blur(模糊)滤镜(smartblur),然后curves滤镜在3秒之后
      ffplay -i good_scale.mp4 -vf "smartblur=enable='between(t,1,20)',curves=enable='gte(t,3)':preset=cross_process"
      
    5. 将h264的第一帧保存为图片
      ffmpeg -i test.h264 -vf "select=between(n\,0\,1)" -y test-%2d.jpg
      

技术栈

  • NodejsExpressFFmpegprotobuffer

技术方案

graph LR
  A[浏览器] --> B[Node服务]
  B[Node服务] --> D[Nodejs-shell];
  D[Nodejs-shell] -->|使用shell功能执行ffmpeg指令| E[FFmpeg];
  E -->|转码并保存| G[静态图片/视频资源];
  G -->|资源路径返回| B[Node服务];
  B -->|资源路径返回 浏览器直接访问静态文件资源| A[浏览器];

核心代码

const shell = require('shelljs');
// 执行ffmpeg
shell.exec(`ffmpeg -i topic.h264 -vf "select='between(n,0,1)'" -y -acodec copy topic.jpg`, (error, stdout, stderr) => {});

完整代码

  • 由于该项目被解析文件并非是一整个纯h264的二进制文件,只是对12路摄像头的topic保存有h264内容,所以会有一些解包操作。

目录

├── config
│   ├── config.js       // 阿里云OSS、百度云BOS、serverPort
│   └── topic_list.js   // protobuffer的对应关系配置
├── ebag.js             // 解包文件
├── package.json        
├── proto               // protobuffer的proto
│   ├── ...
│   ├── camera_data.proto
│   └── ...
├── public              // 静态文件
│   ├── h264            // 存储FFmpeg要读取的h264文件
│   ├── images          // FFmpeg转码h264文件后的图片
│   ├── index.html      // 静态访问首页
│   └── static          // 静态资源
├── server.js           // node 服务
├── start_server.sh
├── stop_server.sh
└── topics.js           // protobuffer解析文件

config.js

/* bca-disable */
module.exports = {
    // Alibaba cloud OSS configuration
    'alioss': {
        region: '',
        accessKeyId: '',
        accessKeySecret: '',
    },
    // Baidu cloud OSS configuration
    'bdbos': {
        endpoint: '',
        credentials: {
            ak: '',
            sk: ''
        }
    },
    // Service port
    'port': ''
};

topic_list.js

module.exports = {
    'around_view_camera': {
        type: '****.****.CameraDataPb',
        file: 'camera_data'
    },
    ...
};

ebag.js

const topics = require('./topics');
const topic_list = require('./config/topic_list');
const config = require('./config/config');
const BosSdk = require('@baiducloud/sdk');
const BosClient = BosSdk.BosClient;

class Ebag {
    constructor() {
        this.op_blocks = null;
    }
    async getBuffer(bucket, dir) {
        try {
            let client = new BosClient(config.bdbos);
            let body = await client.getObject(bucket, dir);
            return body;
        } catch (e) {
            console.log(e);
        }
    }

    open_data(bucket, dir, cb) {
        if (bucket !== '0' || dir !== '0') {
            let self = this;
            this.op_blocks = {
                message: [],
                data: {},
            };

            function done(e) {
                if (cb) {
                    cb(e);
                } else {
                    console.log(e);
                }
            }

            this.getBuffer(bucket, dir).then(source => {
                try {
                    self.size = source.http_headers['content-length'];
                    let head = Buffer.allocUnsafe(14);
                    let pos = 0;
                    while (pos < self.size) {
                        let msg = {
                            head: {}
                        };
                        let res1 = source.body.copy(head, 0, pos, pos + 14 + 1);
                        if (res1 !== 14) {
                            throw 'read head err: ' + JSON.stringify(r);
                        }
                        msg.pos = pos;
                        msg.block_size = head.readUInt32LE(0);
                        msg.stamp_low = head.readUInt32LE(4);
                        msg.stamp_high = head.readUInt32LE(8);
                        msg.stamp = msg.stamp_low + (msg.stamp_high * 0x100000000);
                        msg.head.time = new Date(msg.stamp / 1000);
                        msg.topic_size = head.readUInt16LE(12);
                        if (msg.topic_size > 0) {
                            let topic_buf = Buffer.allocUnsafe(msg.topic_size);
                            let res2 = source.body.copy(topic_buf, 0, pos + 14, pos + 14 + msg.topic_size + 1);
                            if (res2 !== msg.topic_size) {
                                throw 'read topic err: ' + JSON.stringify(r);
                            }
                            msg.head.topic = topic_buf.toString();
                        } else {
                            msg.head.topic = '';
                        }
                        self.op_blocks.message.push(msg);
                        let buf_size = msg.block_size - 14 - msg.topic_size;
                        let buf = Buffer.allocUnsafe(buf_size);
                        let res3 = source.body.copy(buf, 0, msg.pos + 14 +
                            msg.topic_size, msg.pos + msg.block_size + 1);
                        let decode_obj = {};
                        if (res3 === buf_size) {
                            let decode_msg = topics.decode(msg.head.topic, buf);
                            let type = '';
                            if (msg.head.topic in topic_list) {
                                type = topic_list[msg.head.topic].type;
                            }
                            decode_obj = {
                                topic: msg.head.topic,
                                decode_msg: decode_msg,
                                type: type
                            };
                            self.op_blocks.data[pos] = decode_obj;
                        }
                        pos += msg.block_size;
                    }
                } catch (err) {
                    done(err);
                    return;
                }
                done(null);
            });
        }
    }
}

module.exports = Ebag;

package.json

{
    "name": "exps",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "start": "nodemon ./server.js",
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "",
    "license": "ISC",
    "dependencies": {
        "@baiducloud/sdk": "^1.0.0-rc.27",
        "ali-oss": "^6.15.2",
        "base64-js": "^1.3.1",
        "body-parser": "^1.19.0",
        "express": "^4.17.1",
        "jpeg-js": "^0.3.6",
        "md5": "^2.3.0",
        "mysql": "^2.17.1",
        "nodemon": "^2.0.7",
        "protobufjs": "^6.8.8"
    },
    "devDependencies": {
        "shelljs": "^0.8.5"
    }
}

index.html

<!DOCTYPE html>
<html style="height: 100%">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
        <link href="./static/ui/json-viewer.css" type="text/css" rel="stylesheet" />
        <link rel="stylesheet" href="./static/ui/bootstrap.min.css">
    </head>
    <body style="height: 99%;">

        <!-- Base template -->
        <div class="carui-show-base">
            <div class="carui-echarts-base">
                <div style="display: inline; position: relative; left: 33px; top: 15px;">
                    <button type="button" class="btn btn-primary btn-sm" data-toggle="modal" data-target="#chooseTpic">选择消息</button>
                </div>
                <div id="container" style="height: 94%"></div>
            </div>
            <div class="carui-data-show">
                <div class="carui-data-show-top" id="camera">
                    <img src="./static/img/none.png" alt="">
                </div>
                <div class="carui-data-show-bot">
                    <pre id="json-renderer"></pre>
                </div>
            </div>
        </div>

        <!--放大图的imgModal-->
        <div class="modal fade bs-example-modal-xl text-center" id="imgModal" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" >
            <div class="modal-dialog modal-xl" style="display: inline-block; width: auto;">
                <div class="modal-content">
                    <img width="100%" height="100%" id="imgInModalID"  
                    class="carousel-inner img-responsive img-rounded" 
                    onclick="closeImageViewer()"
                    onmouseover="this.style.cursor='pointer';this.style.cursor='hand'" 
                    />
                </div>
            </div>
        </div>

        <!-- Loading -->
        <div class="modal fade" id="loading" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" data-backdrop='static'>
            <div class="modal-dialog" role="document" style="margin-top: 23%;">  
                <div class="d-flex justify-content-center align-items-center">
                    <div class="spinner-border text-light" style="width: 4rem; height: 4rem;" role="status">
                    </div>
                </div>
            </div>
        </div>

        <!-- Scrollable modal -->
        <div class="modal fade" id="chooseTpic"  data-backdrop="static" data-keyboard="false"  tabindex="-1">
            <div class="modal-dialog modal-dialog-scrollable">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title" id="staticBackdropLabel">请选择</h5>
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                            <span aria-hidden="true">&times;</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <form id="topicList"></form>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-primary" id="exec_choose" aria-label="Close" data-dismiss="modal">确定</button>
                    </div>
                </div>
            </div>
        </div>

        <script type="text/javascript" src="./static/echarts/jquery.min.js"></script>
        <script type="text/javascript" src="./static/echarts/echarts.min.js"></script>
        <script src="./static/ui/json-viewer.js"></script> 
        <script src="./static/ui/bootstrap.min.js"></script>
        <script type="text/javascript">    

            function loadStart() {
                $('#loading').modal('show');
            }

            function loadDone() {
                setTimeout(function(){
                    $('#loading').modal('hide');
                }, 500)
            }

            //获取url中的参数
            function getUrlParam(name) {
                var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); //构造一个含有目标参数的正则表达式对象
                var r = window.location.search.substr(1).match(reg);  //匹配目标参数
                if (r != null) return unescape(r[2]); return null; //返回参数值
            }

            //显示大图    
            function showimage(data)
            {
                $("#imgModal").find("#imgInModalID").attr("src", data);
                $("#imgModal").modal();
            }

            //关闭大图
            function closeImageViewer(){
                $("#imgModal").modal('hide');
            }

            // 选定topic信息 确定
            $('#exec_choose').click(function() {
                var topiclist = ''; 
                $('input[name="topiclist"]:checked').each(function(){  
                    topiclist += $(this).val() + ','; //向字符串中添加元素
                });
                if (topiclist.length === 0) {
                    alert('请勾选要显示的topic信息!')
                    return
                }
                var bucket = getUrlParam('bucket');
                var dir = getUrlParam('dir');
                if (bucket === '') {
                    alert('请输入Bucket名称!')
                    return
                }
                if (dir === '') {
                    alert('请输入资源目录地址!')
                    return
                } 
                execEcharts(bucket, dir, topiclist)
            })

            // 初始化echarts
            $(function() {
                var bucket = getUrlParam('bucket');
                var dir = getUrlParam('dir');
                if (bucket === '') {
                    alert('请输入Bucket名称!')
                    return
                }
                if (dir === '') {
                    alert('请输入资源目录地址!')
                    return
                } 
                execEcharts(bucket, dir, '')
            })

            // echarts执行方法
            function execEcharts(bucket, dir, topiclist) {
                loadStart();
                // 获取echarts信息
                $.post('/data', {bucket:bucket, dir:dir, topiclist:topiclist}, function(res) {
                    if (res.err === -1) {
                        loadDone();
                        setTimeout(function(){
                            alert(res.msg)
                        }, 500)
                        return
                    }
                    if (res.data != '') {
                        initEcharts(res.data, res.list, res.topic, res.img)
                    }

                    //  topic列表赋值
                    if (res.initTopiclist) {
                        var topicHtml = '';
                        for(var i = 0; i < res.initTopiclist.length; i++){
                            if (res.initTopiclist[i].checked === 0) {
                                topicHtml +=  `<div class="form-group">` + 
                                `<div class="custom-control custom-switch">` + 
                                    `<input type="checkbox" class="custom-control-input" name="topiclist" id="customSwitch` + i + `" value="` + res.initTopiclist[i].name + `">` + 
                                    `<label class="custom-control-label" for="customSwitch` + i + `">` + res.initTopiclist[i].name + `</label>` +
                                `</div></div>`
                            } else {
                                topicHtml +=  `<div class="form-group">` + 
                                `<div class="custom-control custom-switch">` + 
                                    `<input type="checkbox" class="custom-control-input" name="topiclist" id="customSwitch` + i + `" value="` + res.initTopiclist[i].name + `" checked>` + 
                                    `<label class="custom-control-label" for="customSwitch` + i + `">` + res.initTopiclist[i].name + `</label>` +
                                `</div></div>`
                            }
                        }
                        $('#topicList').children().remove();
                        $('#topicList').append(topicHtml)
                    }
                    loadDone();
                })
            }

            function renderItem(params, api)
            {
                var topic_idx = api.value(0)
                var time = api.value(1)
                var point = api.coord([time, topic_idx]);
                var height = api.size([0, 1])[1] * 0.6;

                point[0] -= 2;
                if (point[0] < 160) {
                    point[0] = 160;
                }

                var rectShape = echarts.graphic.clipRectByRect({
                    x: point[0],
                    y: point[1] - height / 2,
                    width: 2,
                    height: height
                }, {
                    x: params.coordSys.x,
                    y: params.coordSys.y,
                    width: params.coordSys.width,
                    height: params.coordSys.height
                })

                if (!rectShape) {
                    return {
                        type: 'rect',
                        shape: {x:0, y:0, width:0, height:0},
                        style: api.style
                    }
                }

                return rectShape && {
                    type: 'rect',
                    shape: rectShape,
                    style: api.style()
                }
            }

            function initEcharts(data, list, topicData, topicPic) {
                var dom = document.getElementById("container");
                var myChart = echarts.init(dom);
                var app = {};
                var option;
                var startTime = data[0]['value'][1];
                var categories = list;
                option = {
                    tooltip: {
                        trigger: 'item',
                        formatter: function (params) {},
                        axisPointer: {
                            type: 'cross',
                            snap: true,
                            label: {
                                formatter: params => {
                                    if (typeof params.value == 'number') {
                                        // x轴,时间
                                        let date = new Date(params.value);
                                        let t = [date.getHours(), date.getMinutes(), date.getSeconds()]
                                        let d = [date.getFullYear(), date.getMonth()+1, date.getDate()]
                                        let r = d.join('-') + ' ' + t.join(':') + '.' + date.getMilliseconds()
                                        return r
                                    }
                                    return params.value
                                }
                            }
                        }
                    },
                    grid: {
                        left: 160,
                        tooltip: {
                            trigger: 'axis',
                            formatter: 'fff'
                        }
                    },
                    xAxis: {
                        type: 'value',
                        scale: true,
                        min: startTime,
                        axisLabel: {
                            formatter: (value, index) => {
                                let date = new Date(value);
                                let t = [date.getHours(), date.getMinutes()];
                                let r = t.join(':')
                                if (index === 0) {
                                    let d = [date.getMonth()+1, date.getDate()]
                                    r = d.join('-') + ' ' + r
                                }
                                return r;
                            }
                        }
                    },
                    yAxis: {
                        type: 'category',
                        position: 'left',
                        maxInterval: 1,
                        data: categories
                    },
                    toolbox: {
                        feature: {
                            dataZoom: {

                            }
                        }
                    },
                    series: [{
                        type: 'custom',
                        renderItem: renderItem,
                        itemStyle: {
                            color: "#000"
                        },
                        encode: {
                            x: [1],
                            y: [0]
                        },
                        data: data
                    }],
                    dataZoom: [
                        {
                            type: 'slider',
                            show: true,
                            xAxisIndex: [0],
                            start: 0,
                            end: 5,
                        }, {
                            type: 'inside',
                            xAxisIndex: [0],
                            start: 0,
                            end: 5,
                        },
                         {
                            type: 'slider',
                            show: true,
                            yAxisIndex: [0],
                            left: '93%',
                            start: 0,
                            end: 50,
                        }, {
                            type: 'inside',
                            yAxisIndex: [0],
                            start: 0,
                            end: 50,
                            // zoomOnMouseWheel: false,
                            // moveOnMouseWheel: false,
                        }
                    ]
                };

                myChart.setOption(option);

                myChart.off("mouseover");   //防止累计触发   

                myChart.on('mouseover',  function(param) {
                    var pos = param.data.value[2];
                    var name = param.data.name;
                    var showData = topicData[pos]

                    // 有数据渲染数据
                    if (showData) {
                        var options = {
                            collapsed: $('#collapsed').is(':checked'),
                            withQuotes: $('#with-quotes').is(':checked')
                        };
                        $('#json-renderer').jsonViewer(showData, options);
                    }

                    // 有图片渲染图片
                    if (topicPic[pos]) {
                        var src = topicPic[pos];
                        var imgSource = `<img onmouseover="this.style.cursor='pointer';
                        this.style.cursor='hand'" src=` + src + `
                        onclick="javascript:showimage(this.src);" />`
                        // bca-disable-line
                        $('#camera').html(imgSource);
                    } else {
                        var src = './static/img/none.png';
                        var imgSource = `<img src=` + src + ` />`
                        // bca-disable-line
                        $('#camera').html(imgSource);
                    }
                });

                myChart.off("click");   //防止累计触发  

                myChart.on('click', function(param) {

                    // 只有topic以 _record 结尾的 才可以执行图像标记行为
                    if (param.name.indexOf('_record') != -1) {

                        try {
                            var pos = param.data.value[2];

                            // 计算就近perception_view
                            let lastPos = [];
                            let tmpPos = 0;
                            $.each(topicData, function (index, value) {
                                // topic 必须为 perception_view
                                if (topicData[index].topic == 'perception_view') {
                                    // 获取小于当前pos的perception_view信息
                                    if (index <= pos) {
                                        tmpPos = pos - index;
                                        lastPos[index] = tmpPos;
                                    }
                                }
                            });

                            // 就近的 perception_view 信息
                            let perceptionData = topicData[lastPos.length - 1];

                            // 图片信息
                            let obstacleData = topicPic[pos];

                            // 障碍物信息获取
                            let obstaclesData = [];

                            // 可行驶区域标记
                            let freeSpaceData = [];

                            // 不存在障碍物信息 无需标记
                            if (perceptionData.decode_msg.hasOwnProperty('obstacles')) {
                                let obstacleInfo = perceptionData.decode_msg.obstacles;
                                if (obstacleInfo.length < 0) alert('无感知输出信息!无法标记');
                                for (key in obstacleInfo) {
                                    // 判断当前topic信息是否与障碍物摄像头匹配
                                    if (obstacleInfo[key].cameraName === param.name.replace('_record', '')) {
                                        let tmpObstacle = {
                                            'cameraName' : obstacleInfo[key].cameraName,
                                            'imageH' : obstacleInfo[key].imageH,
                                            'imageW' : obstacleInfo[key].imageW,
                                            'imageX' : obstacleInfo[key].imageX,
                                            'imageY' : obstacleInfo[key].imageY,
                                        }
                                        obstaclesData.push(tmpObstacle)
                                    }
                                }
                            }

                            // 不存在可行驶区域信息 无需标记
                            if (perceptionData.decode_msg.hasOwnProperty('freeSpace')) {
                                if (perceptionData.decode_msg.freeSpace.hasOwnProperty('ptsImageCoord')) {
                                    let freeSpaceInfo = perceptionData.decode_msg.freeSpace.ptsImageCoord;
                                    if (freeSpaceInfo.length < 0) alert('无可行驶区域输出信息!无法标记');
                                    for (ki in freeSpaceInfo) {
                                        // 判断当前topic信息是否与障碍物摄像头匹配
                                        if (freeSpaceInfo[ki].cameraName === param.name.replace('_record', '')) {
                                            let tmpFreeSpace = {
                                                'x' : freeSpaceInfo[ki].point.x,
                                                'y' : freeSpaceInfo[ki].point.y,
                                            }
                                            freeSpaceData.push(tmpFreeSpace)
                                        }
                                    }
                                }
                            }

                            // 如果障碍物信息与可行驶区域信息都为空 则无需执行标记
                            if (obstaclesData.length <= 0 && freeSpaceData.length <= 0) {
                                alert('无信息可标记,请换一个再试');
                                return
                            }

                            // 发送至node 处理标记信息
                            $.post('/mark', {
                                perdata: JSON.stringify(obstaclesData), 
                                obdata: obstacleData, 
                                freedata: JSON.stringify(freeSpaceData),
                            }, function(res) {
                                if (res.code == 1) {
                                    var src = res.output;
                                    var imgSource = `<img onmouseover="this.style.cursor='pointer';
                                    this.style.cursor='hand'" src=` + src + `
                                    onclick="javascript:showimage(this.src);" />`
                                    // bca-disable-line
                                    $('#camera').html(imgSource)
                                } else {
                                    alert(res.msg);
                                    return
                                }
                            })

                        } catch(err) {
                            alert('当前数据块无图片信息或无障碍物信息!' + err)
                        }
                    } else {
                        alert('当前数据块无图片信息或无障碍物信息!')
                    }
                })

                if (option && typeof option === 'object') {
                    myChart.setOption(option);
                }

                // 窗口大小改变事件
                window.onresize = function () {
                    myChart.resize();
                };
            }

        </script>
    </body>
</html>

server.js

const express = require('express');
const bodyParser = require('body-parser');
const config = require('./config/config');
const Ebag = require('./ebag');
const jpeg = require('jpeg-js');
const base64 = require('base64-js');
// const cv = require('opencv');
const fs = require('fs');
const path = require('path');
const md5 = require("md5");
const app = express();

const shell = require('shelljs');
let g_ebag = new Ebag();
let g_app = {topic_show_list: ''};

app.use(bodyParser.urlencoded({extended: false, limit: '20mb'}));

app.use('/', express.static('public'));

app.post('/data', function (req, res) {
    let bucket = req.body.bucket;
    let dir = req.body.dir;
    let topicList = req.body.topiclist;
    let topicConf = [];
    let initTopiclist = [];
    let h264File = md5(dir.split('/')[1]);

    if (bucket === '0' || dir === '0') {
        res.send({'data': [{'name': 'Please', 'value': [-1, Date.now() - 1, '']},
        {'name': 'Please', 'value': [-1, Date.now(), '']}],
        'list': ['Please', 'enter', 'cloud', 'information', '.']});
        return;
    }

    if (topicList !== '') {
        topicConf = topicList.substr(0, topicList.length - 1).split(',');
    }
    g_ebag.open_data(bucket, dir, err => {
        if (err) {
            let result = {'err': -1, 'msg': '读取文件失败,请检查bucket与资源目录地址是否有误'};
            res.send(result);
        } else {
            // 数据 x/y 数据
            g_ebag.topic_msg = {};
            for (let msg of g_ebag.op_blocks.message) {
                let topic = msg.head.topic;
                if (!g_ebag.topic_msg[topic]) {
                    g_ebag.topic_msg[topic] = [];
                }
                g_ebag.topic_msg[topic].push(msg);
            }
            g_app.topic_show_list = Object.keys(g_ebag.topic_msg).sort();
            // 显示图表
            let topic_index = {};
            let data = [];
            let idx = 0;
            // 如果topiclist不为空 计算与g_app.topic_show_list的交集 过滤展示的topic
            if (topicConf.length !== 0) {
                /**
                 * 重组选择消息列表 已选择 为checked
                 */
                g_app.topic_show_list.forEach(function (value, index) {
                    initTopiclist[index] = {
                        'name': value,
                        'checked': (topicConf.indexOf(value) === -1) ? 0 : 1
                    };
                });
                g_app.topic_show_list = topicConf.filter(function (val) {
                    return g_app.topic_show_list.indexOf(val) > -1;
                });
            } else {
                g_app.topic_show_list.forEach(function (value, index) {
                    initTopiclist[index] = {'name': value, 'checked': 0};
                });
            }

            // 画图参数 及 自定义echarts数据组装
            g_app.topic_show_list.forEach(function (topic, index) {
                topic_index[topic] = idx++;
                for (let msg of g_ebag.topic_msg[topic]) {
                    let tm = Number(msg.head.time);
                    data.push({
                        name: topic,
                        value: [
                            topic_index[topic],
                            tm,
                            msg.pos
                        ]
                    });
                }
            });
            // 获取topic信息的data
            let topicData = g_ebag.op_blocks.data;
            // 返回图片信息
            let topicPic = {};
            /* bca-disable */
            for (let key in topicData) {
                // 有图片数据 需要处理图片相应的数据
                switch (topicData[key].type) {
                    case '****.****.CameraDataPbDev': // BGR -> RGB -> jpeg -> base64
                        let img = {
                            data: [],
                            height: topicData[key].decode_msg.height,
                            width: topicData[key].decode_msg.width,
                        };
                        for (let i = 0, j = 0; i < topicData[key].decode_msg.data.length; i += 3, j += 4)
                        {
                            img.data[j + 0] = topicData[key].decode_msg.data[i + 2];
                            img.data[j + 1] = topicData[key].decode_msg.data[i + 1];
                            img.data[j + 2] = topicData[key].decode_msg.data[i + 0];
                            img.data[j + 3] = 0xff;
                        }
                        let rawImageData = {
                            data: img.data,
                            width: topicData[key].decode_msg.width,
                            height: topicData[key].decode_msg.height
                        };
                        let jpg_data_a = jpeg.encode(rawImageData, 50);
                        let jpg_data_base_a = base64.fromByteArray(jpg_data_a.data);
                        topicPic[key] = jpg_data_base_a;
                        delete topicData[key].decode_msg.data;
                        break;

                    case '****.****.CameraCompressDataPb':
                        let raw = jpeg.decode(topicData[key].decode_msg.data, true);
                        for (let i = 0; i < raw.data.length; i += 4) {
                            let t = raw.data[i + 0];
                            raw.data[i + 0] = raw.data[i + 2];
                            raw.data[i + 2] = t;
                        }
                        let jpg_data_b = jpeg.encode(raw, 50);
                        let jpg_data_base_b = base64.fromByteArray(jpg_data_b.data);
                        topicPic[key] = jpg_data_base_b;
                        delete topicData[key].decode_msg.data;
                        break;

                    case '****.****.CameraDataPb':
                        let baseFileName = h264File + '_' + key;
                        let inputFile = path.join(__dirname, 'public/h264/' + baseFileName);
                        let outputFile = path.join(__dirname, 'public/images/' + baseFileName);
                        let imageUrl = 'images/' + baseFileName + '/topic.jpg';

                        let dataBuffer = topicData[key].decode_msg.data;
                        const isExistInputFile = fs.existsSync(inputFile)
                        const isExistOutputFile = fs.existsSync(outputFile)
                        if (isExistInputFile && isExistOutputFile) {
                            // 如果存在直接读取
                            topicPic[key] = imageUrl;
                            break;
                        } else {
                            // 如果不存在 删除已有文件
                            if (!isExistInputFile && isExistOutputFile) {
                                fs.unlink(outputFile + '/topic.jpg', function (err, data) {});
                                fs.rmdirSync(outputFile, {recursive: true});
                            }
                            if (!isExistOutputFile && isExistInputFile) {
                                fs.unlink(inputFile + '/topic.h264', function (err, data) {});
                                fs.rmdirSync(inputFile, {recursive: true});
                            }
                            fs.mkdirSync(inputFile)
                            fs.mkdirSync(outputFile)
                            fs.writeFile(inputFile + '/topic.h264', dataBuffer, function(err){
                                if(!err){
                                    // 执行ffmpeg
                                    shell.exec(`ffmpeg -i public/h264/'` + baseFileName + `'/topic.h264 -vf "select='between(n,0,1)'" -y -acodec copy public/images/'` + baseFileName + `'/topic.jpg`, (error, stdout, stderr) => {});
                                }
                            })
                            topicPic[key] = imageUrl;
                            break;
                        }
                    default:
                        break;
                }
            }

            let result = {
                'err': 0,
                'data': data,
                'list': g_app.topic_show_list,
                'topic': topicData,
                'img': topicPic,
                'initTopiclist': initTopiclist
            };
            res.send(result);
        }
    });
});

app.post('/mark', function (req, res) {
    let obstacle = JSON.parse(req.body.perdata);
    let freespace = JSON.parse(req.body.freedata);
    let imgData = req.body.obdata;
    let date = Date.now();

    // base64 存储到 文件
    let path = './public/sourceimg/' + date + '.jpeg';

    // 过滤data:URL
    let base64Data = imgData.replace(/^data:image\/\w+;base64,/, '');
    let dataBuffer = Buffer.from(base64Data, 'base64');
    fs.writeFile(path, dataBuffer, function (err) {
        if (err) {
            res.send({'code': '-1', 'msg': '图片处理失败!'});
        }

        障碍物标记 - 输出图片
        let outputPath = './public/obstaclesimg/' + date + '.jpeg';

        // 处理图片
        /* bca-disable */
        // cv.readImage(path, function (err, im) {

        //     // 标记障碍物信息
        //     obstacle.forEach(function (value, index) {
        //         im.rectangle([value.imageX, value.imageY], [value.imageW, value.imageH], [0, 255, 0], 2);
        //     });

        //     // 标记可行驶区域信息
        //     /* bca-disable */
        //     freespace.forEach(function (val, idx) {
        //         im.rectangle([val.x, val.y], [3, 3], [0, 255, 0], 3);
        //     });
        //     im.save(outputPath);
        // });

        res.send({'code': '1', 'output': '/obstaclesimg/' + date + '.jpeg'});
    });
});

app.listen(config.port);

topics.js

const protobuf = require('protobufjs');
let topic_list = require('./config/topic_list.js');
function load_pbs()
{
    /* bca-disable */
    for (let topic in topic_list) {
        let detail = topic_list[topic];
        let r = protobuf.loadSync(`${__dirname}/proto/${detail.file}.proto`);
        detail.froot = r;
        detail.pbType = r.lookupType(detail.type);
    }
}
load_pbs();

function decode(topic, buf)
{
    let detail = topic_list[topic];
    if (!detail) {
        // console.error("not found topic: ", topic)
        // 使用空消息解析,只解出消息头
        detail = topic_list.empty;
        if (!detail) {
            return {};
        }
    }
    try {
        let o = detail.pbType.decode(buf);
        return o;
    } catch (e) {
        // console.log('pb decode err:', topic, e, buf.toString('hex'))
        return {};
    }
}

module.exports = {
    decode,
};

访问结果

Image title

范例

Image title

范例

拓展

  • 使用JavaScript播放H264视频 地址