基于librtmp的安卓小项目:投屏摄像头和手机拼屏幕视频:推流rtmp到服务器上并显示在其它设备上(比如电脑或者其它直播平台)

首先这个项目并未实现音频的传输,后面有时间再实现音频的传输后更新博文。这里如果是自己部署流媒体服务器,可以参考搭建nginx的相关博文,这里需要注意的是如果是搭建在linux系统下面,那么网络最好选用桥接模式,因为nat模式底下网络ip和手机不在同一个网段的,可以结合ffmpeg的ffplay(笔者为了项目整体性是用qt直接写好对应ffmpeg小型播放器)来测试播放效果。如果jni+cmake配置有问题的可以参考之前笔者博文,mediacodec有问题的话可以参考网上一些代码和笔者之前的踩坑日志,上面贴一个app封面的图,这里感谢雷神的部分演示代码和其它一些博文,不过事实上雷神的部分代码可读性不强(也可能我大四才疏学浅),其它某些博文也只是照搬东西,因此笔者对此进行一些改造后可以使用在该项目上。

        要实现这项功能的大致流程是:在android studio那边要做好java层和c++层的连接,java层需要将推流的url地址先传给c++层进行rtmp_init,然后mediacodec层开始编码时,将视频数据、时间戳(pts)(这个其实也就是描述视频在什么时间点显示的相关参数,是递增的)、视频长度(此参数其实在c++层调用api来获取也可以)传给c++层,然后组包用RTMP_SEND送给rtmp服务器,然后在其它设备显示出来。

        这里“送给rtmp服务器”的过程是有特点的:首先第一帧的数据可以获取sps/pps的信息,(这部分不推荐用雷神代码,太多while循环容易头晕)然后后面如果是关键帧,那么要先发送一下sps/pps的信息,然后再发送其余帧信息,如果不是关键帧直接把帧信息组包发出去就完事,这里就要注意你不能说在编码线程运行中间突然开启rtmp推流,这样就会错过sps/pps信息,服务器就不能正常收到信息,此时一般要先重置编码器然后再开启。

        这里的“sps/pps”的长度以及帧信息是“不包含分割符的”,一般安卓手机摄像头每两帧之间都是用分割字符0x00,0x00,0x00,0x01来分开的,我们拿到的手机的帧信息(mediacodec解码后存储在bytebuffer中)要先把头部给拿掉,然后再做其它操作。

        一般帧数据头部含分隔符的前5个数据通常为:0x00,0x00,0x00,0x01,0x67(sps)、0x00,0x00,0x00,0x01,0x68(pps)、0x00,0x00,0x00,0x01,0x41(普通帧)和0x00,0x00,0x00,0x01,0x65(关键帧),所以只要根据第5个数据就可以判断该帧数据里面放的数据信息属性。

    然后时间戳的话,要先记录第一帧的bufferInfo.presentationTimeUs然后用后面的presentationTimeUs扣掉第一帧的这个参数获取时间戳,据说一般手机摄像头的时间戳在30左右(笔者是33这样)。

         这样代码的大致思路就清楚了,在写jni代码之前,首先为了方便打印日志,这边笔者用一个这个printLog函数来打印一些重要信息(%x一样可以打印十六进制数据,这里就不P出来了)。要注意这个方法打印数据时一定会回车打印,打印十六进制数据需要另外多输入几个%x。

#include <android/log.h>
void printLog(const char* _log)
{
    __android_log_print(ANDROID_LOG_INFO, "lclclc", "%s\n", _log); //log i类型
}

void printLog(int _log)
{
    __android_log_print(ANDROID_LOG_INFO, "lclclc", "%d\n", _log); //log i类型
}

初始化和close RTMP函数如下:(这里用Live结构体存储sps pps数据)

typedef struct
{
    int16_t sps_len;
    int16_t pps_len;
    int8_t *sps;
    int8_t *pps;
    RTMP *rtmp;
}Live;

RTMP* initRTMP(const char* url)
{
    RTMP* rtmp=RTMP_Alloc();
    RTMP_Init(rtmp);
    rtmp->Link.timeout=10;
    int ret=RTMP_SetupURL(rtmp,(char*)url);
    if(!ret)
    {
        printLog("init URL fail");
        return NULL;
    }
    RTMP_EnableWrite(rtmp);
    ret=RTMP_Connect(rtmp,0);
    if(!ret)
    {
        printLog("Connect fail");
        return NULL;
    }
    ret=RTMP_ConnectStream(rtmp,0);
    if(!ret)
    {
        printLog("connect URL fail");
        return NULL;
    }
    live=(Live*)malloc(sizeof(Live));
    memset(live,0,sizeof(Live));
    live->rtmp=rtmp;
    return rtmp;
}

void closeRTMP(RTMP* rtmp)
{
    if(rtmp!=NULL)
    {
        RTMP_Close(rtmp);
        RTMP_Free(rtmp);
        free(live);
    }
}

然后我们要把sps.pps帧的信息提取出来,这里不用雷神的代码,P上我自己的代码


void preparevideo(int8_t* data,int len)
{
    int i=3;
    //首先是sps帧,我们要找到pps帧的头部位置
    while(i<len)
    {
        if(i+4<len)
        {
            if(data[i]==0x00&&data[i+1]==0x00&&data[i+2]==0x00&&data[i+3]==0x01&&data[i+4]==0x68)
            {
                //这样sps的长度和大小就得到了
                live->sps_len=i-3;
                live->sps=(int8_t *) malloc(live->sps_len+1);
                memset(live->sps,0,sizeof(live->sps));
                memcpy(live->sps,data+4,live->sps_len);
                //后面计算pps长度和获取内容
                live->pps_len=len-i-4;
                live->pps=(int8_t *) malloc(live->pps_len + 1);
                memset(live->pps,0,sizeof(live->pps));
                memcpy(live->pps,&data[i+4],live->pps_len);
                break;
            }
        }
        ++i;
    }
}

视频组包那边就直接用雷神的代码来搞了,组包格式如下:(从第一个字符开始)

1.关键帧:0x17;非关键帧:0x27

2.3.4.5.如果是sps/pps数据:0x00 0x00 0x00 0x00

如果是帧数据:0x01 0x00 0x00 0x00

6.7.8.9:如果是帧数据:待传输的数据长度的十六进制表示

如果是sps/pps数据那么比较复杂,后面另外陈述。

10以后:帧数据。

sps/pps的话:

版本号:0x01(占一个字节)、编码规格(sps[1].sps[2].sps[3]拼在一起占三个字节)(这里注意sps[1]不是0x67之类的,它一般是11的倍数)、几个字节的nalu包的长度、sps个数(占一个字节)、sps长度(占2字节)、sps内容、pps个数(1字节)、pps长度(2字节)、pps内容。

这里网上几乎都能copy到相关代码,内容大同小异,这里就不演示了。

此外这里为了接收Java层传输过来的jbyte数据,用了一个工具方法:


char* ConvertJByteaArrayToChars(JNIEnv *env, jbyteArray bytearray)
{
    char *chars = NULL;
    jbyte *bytes;
    bytes = env->GetByteArrayElements(bytearray, 0);
    int chars_len = env->GetArrayLength(bytearray);
    chars = new char[chars_len + 1];
    memset(chars,0,chars_len + 1);
    memcpy(chars, bytes, chars_len);
    chars[chars_len] = 0;

    env->ReleaseByteArrayElements(bytearray, bytes, 0);

    return chars;
}

接下来是发送数据的核心代码:


extern "C"
JNIEXPORT void JNICALL
Java_com_example_mediaitem2_AvcEncoder_RTMP_1SEND(JNIEnv *env, jobject thiz, jbyteArray data,
                                                  jint byte_len, jboolean is_key_frame,
                                                  jlong time_stamp) {
    // TODO: implement RTMP_SEND()

    char* bytePtr=ConvertJByteaArrayToChars(env,data);

    printLog(byte_len);
    __android_log_print(ANDROID_LOG_INFO, "lclclc", "%x%x%x%x%x%x\n",
            bytePtr[0],bytePtr[1],bytePtr[2],bytePtr[3],bytePtr[4],bytePtr[5]); //log i类型

    if(bytePtr[4]==0x67)
    {
        printLog("记录pps帧和sps帧数据");
        preparevideo(reinterpret_cast<int8_t *>(bytePtr), byte_len);
        char* body=(char*)live->sps;
//        __android_log_print(ANDROID_LOG_INFO, "lclclc","%x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x\n",
//                            body[0],body[1],body[2],body[3],body[4],body[5],body[6],body[7],body[8],body[9],body[10],
//                            body[11],body[12],body[13],body[14],body[15],body[16],body[17]);

        body=(char*)live->pps;
    }
    else
    {
        if(bytePtr[4]==0x65)
        {
            int ret=SendH264Packet(&bytePtr[4],byte_len-4,1,time_stamp);
            if(ret!=1)
            {
                printLog("发送失败");
            }
        }
        else
        {
            int ret=SendH264Packet(&bytePtr[4],byte_len-4,0,time_stamp);
            if(ret!=1)
            {
                printLog("发送失败");
            }
        }
    }


}

java层encoded输出数据循环的处理,供参考:


                            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
                            int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
                            while (outputBufferIndex >= 0) {
                                if(isFirstFrame&&isRTMP)
                                {
                                    startTime=bufferInfo.presentationTimeUs/1000;
                                    isFirstFrame=false;
                                }
                                //Log.i("AvcEncoder", "Get H264 Buffer Success! flag = "+bufferInfo.flags+",pts = "+bufferInfo.presentationTimeUs+"");
                                ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
                                byte[] outData = new byte[bufferInfo.size];
                                outputBuffer.get(outData);
//                                System.out.println("转换成十六进制数据为:"+bytes2hex(outData));
//                                System.out.println("时间戳PTS为:"+(bufferInfo.presentationTimeUs/1000-startTime));

                                if(bufferInfo.flags == BUFFER_FLAG_CODEC_CONFIG){
                                    configbyte = new byte[bufferInfo.size];
                                    configbyte = outData;
                                    if(isRTMP)
                                        RTMP_SEND(outData,bufferInfo.size,false,bufferInfo.presentationTimeUs/1000-startTime);
                                }else if(bufferInfo.flags == BUFFER_FLAG_KEY_FRAME){
                                    System.out.println("这帧是关键帧");
                                    byte[] keyframe = new byte[bufferInfo.size + configbyte.length];
                                    System.arraycopy(configbyte, 0, keyframe, 0, configbyte.length);
                                    //把编码后的视频帧从编码器输出缓冲区中拷贝出来
                                    System.arraycopy(outData, 0, keyframe, configbyte.length, outData.length);
                                    if(isRTMP)
                                        RTMP_SEND(outData,outData.length,true,bufferInfo.presentationTimeUs/1000-startTime);
                                    if(isRecording)
                                        outputStream.write(keyframe, 0, keyframe.length);
                                }else{
                                    //写到文件中
                                    if(isRTMP)
                                        RTMP_SEND(outData,bufferInfo.size,false,bufferInfo.presentationTimeUs/1000-startTime);
                                    if(isRecording)
                                        outputStream.write(outData, 0, outData.length);
                                }

                                mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
                                outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);

此外,相同的方法也可以把手机屏幕投屏上传上去,需要用到mediaProjection类,具体可以百度了解一下用法,这里申请权限时需要注意grable版本务必在28或以下,否则会无法正常申请权限(这个是安卓内部的bug)。然后这里还要通过mediacodec.getInputSurface来做,这里getInputSurface必须在configure后立即调用,并在打开解码器线程前调用好屏幕VirtusDisplay的相关接口(否则会抛出illegalation State 0x06异常),然后既然已经用了surface来输入,在解码线程就别再写什么dequeInputBuffer之类的了,否则也是会抛出异常。在测试时可能会出现开始没有屏幕信息的情况,因为屏幕录制是等屏幕有变化时调整录制信息,屏幕无像素变化时不会发送编码信息,滑动几下屏幕一般就能看到画面。

此处也copy上qt播放端用ffmpeg写的代码,除了路径以外可以直接拷贝使用(filePath要自己配置一下):(如果对配置ffmpeg+qt有问题的可以参考一下网上其它博文配置一下环境,觉得麻烦的也可以直接下载个ffmpeg用命令行,当然也听说有人用VLC播放器拉流,这个我也没用过)

头文件内容:

#ifndef WIDGET_H
#define WIDGET_H
#define _FIRST 1
#define _ZERO 0
#include <QWidget>
#include<QDebug>
#include<QPainter>
#include<QPaintEvent>
#include<QLabel>
#include<QCoreApplication>
#include<QTime>
#include<QTimer>
#include<QCameraInfo>
#include<iostream>
#include<QSlider>
extern "C"{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavdevice/avdevice.h>
#include <libavformat/version.h>
#include <libavutil/time.h>
#include <libavutil/mathematics.h>
#include <libavutil/imgutils.h>
}
using namespace std;
class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = 0);
    QLabel *label,*timeLabel;

    QTimer timer,timer2;
    QSlider *slider;

    bool flag;

    uint8_t* buffer;
    AVFrame *pFrameRGB;

    //保存文件容器封装信息和码流参数的结构体
    AVFormatContext *pFormatCtx=NULL;
    //解码器上下文对象,解码器依赖的相关环境,状态,资源以及参数集接口指针
    AVCodecContext *pCodecCtx=NULL;
    AVPacket *packet;
    //提供编码器和解码器的公共接口
    AVCodec *pCodec=NULL;
    //保存音视频解码后的数据,包括状态信息,编码解码信息,QP表,宏块类型表,运动矢量表灯
    AVFrame *pFrame=NULL;
    //描述转换器参数的结构体
    struct SwsContext *sws_ctx=NULL;
    AVDictionary *optionsDict=NULL;
    //循环变量,视频流类型编号
    int i,videoStream;
    //解码操作成功标识
    int frameFinished;
    double frameTime=0;

    int nextValue=0,befoValue=0;

    int curmin=0,curs=0,curhour=0;

    int frameDuration=0;

    ~Widget();
    void Delay(int msec);
    void on_btnPlay_clicke();
    double readFrame();

//    void prepareRead();
//    void paintEvent(QPaintEvent *event);
    void paint();

public slots:
    void setFrame(QPixmap &pixmap);
};

#endif // WIDGET_H

cpp代码:

#include "widget.h"

Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
    label=new QLabel(this);
    label->setGeometry(0,0,this->width(),this->height()-50);
    label->show();


    slider=new QSlider(Qt::Horizontal,this);
    slider->setValue(0);
    slider->setGeometry(0,this->height()-40,this->width(),20);
    slider->setMinimum(0);
    slider->show();

    timeLabel=new QLabel(this);
    timeLabel->setGeometry(this->width()-50,this->height()-20,50,20);
    timeLabel->setText("0:0:0");


//    connect(&timer2,&QTimer::timeout,[=](){
//        flag=false;
//        timer2.stop();
//    });

    connect(slider,&QSlider::valueChanged,[=](){

        timer.stop();
        //当前进度条的读数
        int currValue=slider->value();
        //转换成当前时间=总时间*当前读数/总数
        int curTimes=currValue/1000000;
        //计算pts
//        int pts=curTimes;
        qDebug()<<"pts="<<curTimes/av_q2d(pFormatCtx->streams[videoStream]->time_base);
        //跳转帧
        int ret=av_seek_frame(pFormatCtx,videoStream,/*1000000*curTimes/19*/curTimes/av_q2d(pFormatCtx->streams[videoStream]->time_base),AVSEEK_FLAG_ANY);

        qDebug()<<curTimes;
        //1000000   42-24=18
        if(ret<0)
        {
            qDebug()<<"fail";
            return;
        }

        //底下的时间也要跟着改
        //秒
        int s=curTimes%60;
        int min=curTimes-s;
        int hour=0;
        if(min>=3600)
        {
            int i=0;
            while(min-3600*i>=60&&++i);
            min=(min-3600*i)/60;
            hour=i;
        }
        else
        {
            min=min/60;
        }
        curs=s;curmin=min;curhour=hour;

        QString timeStr;

        timeStr+=QString::number(curhour);
        timeStr+=":";
        timeStr+=QString::number(curmin);
        timeStr+=":";
        timeStr+=QString::number(curs);

        timeLabel->setText(timeStr);

        befoValue=curTimes;

//        readFrame();

        timer.start();

    });

    on_btnPlay_clicke();





//    prepareRead();

}

Widget::~Widget()
{

}

void Widget::Delay(int msec)
{
    QTime dieTime = QTime::currentTime().addMSecs(msec);
    while( QTime::currentTime() < dieTime )
        QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
}

void Widget::on_btnPlay_clicke()
{
     QString cameraName;
    //获取摄像头的信息
    QList<QCameraInfo> cameras=QCameraInfo::availableCameras();
    foreach(const QCameraInfo &cameraInfo,cameras)
    {
        //get camera Name
        cameraName=cameraInfo.description();
        qDebug()<<cameraName;
    }


    //注册所有ffmpeg支持的多媒体格式及编解码器
    av_register_all();

    AVInputFormat *inputFormat = av_find_input_format("dshow");

    string pathName="video=";
    pathName+=cameraName.toStdString();

    AVDictionary* options=NULL;
    av_dict_set(&options,"buffer_size","1024000",0);
    av_dict_set(&options,"max_delay","500000",0);
    av_dict_set(&options,"stimeout","20000000",0);
    av_dict_set(&options,"rtsp_transport", "tcp", 0);

    pathName="rtmp://192.168.0.103:1935/live";
//    pathName="E://video.wmv";
//    pathName="rtsp://192.168.78.128:8554/test.264";
//    pathName="rtmp://192.168.78.128:8554/livestream";

//    pathName="rtmp://sendtc3a.douyu.com/live/11387593rZqiBvMZ?wsSecret=b5f70a2285186942e1e4c2be84b78ae4&wsTime=63c4e15d&wsSeek=off&wm=0&tw=0&roirecognition=0&record=flv&origin=tct&txHost=sendtc3a.douyu.com";
//    pathName="E://test1.avi";
    //打开视频文件,读文件头内容,取得文件容器的封装信息及码流参数并存储在pFormatCtx中
    if(avformat_open_input(&pFormatCtx,(const char*)pathName.data(),NULL,&options)!=0)
    {
        qDebug()<<"open file fail";
        return ;
    }
    //尝试获取文件中保存的码流信息,并填充到pFormatCtx->stream字段中
    if(avformat_find_stream_info(pFormatCtx,NULL)<0)
    {
        qDebug()<<"get code stream fail";
        return;
    }

    videoStream=-1;
    //确认文件的所有流媒体类型中包含视频流类型
    for(i=0;i<pFormatCtx->nb_streams;++i)
    {
        if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO)
        {
            videoStream=i;
            break;
        }
    }
    //如果没有找到视频流就退出
    if(videoStream==-1)
    {
        qDebug()<<"no video stream";
        return;
    }
//    videoStream=av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
    //根据流类型标号从pFormatCtx->streams中取得视频流对应的解码器上下文
    pCodecCtx=pFormatCtx->streams[videoStream]->codec;
    //根据视频流对应的解码器上下文查找对应的解码器,返回对应的解码器(信息结构体)
    pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
    //如果找不到符合条件的编码器
    if(pCodec==NULL)
    {
        qDebug()<<"can't find decoder";
        return;
    }


    pFrame = av_frame_alloc();//存放从AVPacket中解码出来的原始数据
    pFrameRGB = av_frame_alloc();//存放原始数据转换的目标数据
    packet = av_packet_alloc();

    av_new_packet(packet, pCodecCtx->width * pCodecCtx->height);//分配packet的有效载荷并初始化其字段

    //打开编码器
    if(avcodec_open2(pCodecCtx,pCodec,&optionsDict)<0)
    {
        qDebug()<<"decoder open fail";
        return;
    }
    // Allocate video frame,为解码后的视频信息结构体分配空间并完成初始化操作(结构体中的图像缓存按照下面两步手动安装)
    //设置图像的转换格式
    sws_ctx=sws_getContext(pCodecCtx->width,pCodecCtx->height,//转换图片之前的宽高、格式和之后的高宽和格式
                           pCodecCtx->pix_fmt,pCodecCtx->width,pCodecCtx->height,
                           AV_PIX_FMT_RGB32,SWS_BICUBIC,NULL,NULL,NULL);

    int numBytes = avpicture_get_size(AV_PIX_FMT_RGB32, pCodecCtx->width,pCodecCtx->height);//(弃用函数)
//     qDebug() << numBytes;


//    buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));
//         /*瓜分分配的空间*/
//         //瓜分上一步分配到的buffer.



//    if(av_image_fill_arrays(pFrameRGB->data,   // 需要填充的图像数据指针
//                         pFrameRGB->linesize,
//                         buffer,
//                         AV_PIX_FMT_RGB32, //图像的格式
//                         pCodecCtx->width,
//                         pCodecCtx->height,
//                         1) < 0)    //图像数据中linesize的对齐
//    {
//        qDebug()<<"获取流图像失败";
//        return;
//    }
     auto buf = static_cast<uchar *>(av_malloc(static_cast<size_t>(av_image_get_buffer_size(AV_PIX_FMT_RGB32,
                                                                                            pCodecCtx->width,
                                                                                            pCodecCtx->height, 1))));

//     qDebug()<<"时长为:"<<pFormatCtx->duration/1000000.0;
     qDebug()<<pFormatCtx->duration;
     slider->setMaximum(pFormatCtx->duration);
     qDebug()<<"set value succ";
     //根据后5个参数的内容填充前两个参数,成功返回源图像的大小,失败返回一个负值
     if(av_image_fill_arrays(pFrameRGB->data,   // 需要填充的图像数据指针
                          pFrameRGB->linesize,
                          buf,
                          AV_PIX_FMT_RGB32, //图像的格式
                          pCodecCtx->width,
                          pCodecCtx->height,
                          1) < 0)    //图像数据中linesize的对齐
     {
         qDebug()<<"获取流图像失败";
         return;
     }

//    int y_size = pCodecCtx->width * pCodecCtx->height;
//    packet = (AVPacket *) malloc(sizeof(AVPacket)); //申请一个视频帧包的大小
//    av_new_packet(&packet, y_size); //分配packet的数据,为packet分配一个指定大小的内存
//      avpicture_fill((AVPicture *)pFrameRGB, buffer, AV_PIX_FMT_RGB32, pCodecCtx->width, pCodecCtx->height);

     QObject::connect(&timer,QTimer::timeout,[=](){
         readFrame();
     });

    qDebug()<<pFormatCtx->streams[videoStream]->nb_frames<<pFormatCtx->duration;


    frameTime=1000/pFormatCtx->streams[videoStream]->avg_frame_rate.num;
//    if(pFormatCtx->streams[videoStream]->nb_frames!=0)
//    {
//        frameTime=pFormatCtx->duration/pFormatCtx->streams[videoStream]->nb_frames/1000;
//    }
//    else
//    {
//        frameTime=pFormatCtx->duration/420/1000;
//        qDebug()<<pFormatCtx->streams[videoStream]->avg_frame_rate.num;
//    }
    timer.start();
    //每帧时间(单位:ms)


    qDebug()<<"==========================================================================================";

}

double Widget::readFrame()
{
//    flag=true;

//    timer2.start(frameTime);

    if(av_read_frame(pFormatCtx,packet)>=0)
    {
//        qDebug()<<")))))))))))))";
        if(packet->stream_index==videoStream)
        {
            /*-----------------------
            * Decode video frame,解码完整的一帧数据,并将frameFinished设置为true
            * 可能无法通过只解码一个packet就获得一个完整的视频帧frame,可能需要读取多个packet才行
            * avcodec_decode_video2()会在解码到完整的一帧时设置frameFinished为真
            * Technically a packet can contain partial frames or other bits of data
            * ffmpeg's parser ensures that the packets we get contain either complete or multiple frames
            * convert the packet to a frame for us and set frameFinisned for us when we have the next frame
            -----------------------*/
            //=========================解码方法一==========================================//
//            int ret = avcodec_send_packet(pCodecCtx, packet);                    //发送数据到ffmepg,放到解码队列中
//            if (ret < 0)
//            {
//                qDebug()<<"decode failed";
//                return;
//            }
//            int got_picture = avcodec_receive_frame(pCodecCtx, pFrame);          //将成功的解码队列中取出1个frame
//            if(got_picture)
//            {
//                qDebug()<<"get failed";
//                return;
//            }
            int ret=avcodec_decode_video2(pCodecCtx,pFrame,&frameFinished,packet);

            sws_scale(sws_ctx,
                      static_cast<const uchar* const*>(pFrame->data),
                      pFrame->linesize,
                      0,
                      pCodecCtx->height,
                      pFrameRGB->data,
                      pFrameRGB->linesize);

//            qDebug()<<

            QPixmap pixmap = QPixmap::fromImage(QImage(static_cast<uchar*>(pFrameRGB->data[0]),
                                               pCodecCtx->width,
                                               pCodecCtx->height,
                                               QImage::Format_RGB32).scaled(label->width(),label->height()));
//            label->setPixmap(QPixmap::fromImage(QImage((uchar*)buffer,
//                                                       pFrameRGB->width,
//                                                       pFrameRGB->height,QImage::Format_RGB32)));
//            frameDuration=packet->duration;

//            qDebug()<<packet->dts;

            nextValue=av_q2d(pFormatCtx->streams[videoStream]->time_base)*packet->pts;

//            qDebug()<<packet->fps;

            if(nextValue>=befoValue+1)
            {
                befoValue=nextValue;

                slider->blockSignals(true);

                slider->setValue(av_q2d(pFormatCtx->streams[videoStream]->time_base)*packet->pts*1000000);

                slider->blockSignals(false);

                ++curs;

                if(curs==60)
                {
                    curs=0;
                    ++curmin;
                    if(curmin==60)
                    {
                        curmin=0;
                        ++curhour;
                    }
                }

                QString timeStr;

                timeStr+=QString::number(curhour);
                timeStr+=":";
                timeStr+=QString::number(curmin);
                timeStr+=":";
                timeStr+=QString::number(curs);

                timeLabel->setText(timeStr);
            }


            label->setPixmap(pixmap);
            av_packet_unref(packet);
            Delay(frameTime);
//            Delay(40);
//            }
        }

    }
    else
    {
        qDebug()<<"movie end";
//        av_free(pFrame);
//        avcodec_close(pCodecCtx);
//        avformat_close_input(&pFormatCtx);
        timer.stop();
    }

}

void Widget::paint()
{

}

void Widget::setFrame(QPixmap &pixmap)
{

}
// https://blog.csdn.net/m0_48578207/article/details/107372494

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 要将树莓派摄像头RTMP协议转换为WebRTC协议并推流服务器,可以采取以下步骤: 1. 安装 Janus Gateway Janus Gateway 是一个开源的 WebRTC 服务器,可以将视频流转换为 WebRTC 协议。可以通过以下命令在树莓派上安装 Janus Gateway: ``` sudo apt-get install janus ``` 2. 配置 Janus Gateway 安装完成后,需要对 Janus Gateway 进行一些配置。首先,打开 Janus Gateway 的配置文件 `/etc/janus/janus.cfg`,将以下内容添加到文件末尾: ``` [rtmp-to-webrtc] type = rtmp id = 1 description = RTMP to WebRTC audio = yes video = yes videoport = 5004 videopt = 100 audiport = 5006 audiopt = 111 rtmpUrl = rtmp://localhost/live/test rtmpAudioTrack = 0 rtmpVideoTrack = 1 ``` 这个配置文件指定了将 RTMP 流转换为 WebRTC 流的详细信息。其中,`rtmpUrl` 指定了 RTMP 流的 URL,`videoport` 和 `audioport` 指定了 Janus Gateway 使用的端口号,`videopt` 和 `audiopt` 指定了音视频的负载类型。具体来说,`videopt` 为 100 表示 H.264 编码,`audiopt` 为 111 表示 Opus 编码。 3. 启动 Janus Gateway 启动 Janus Gateway 服务,可以使用以下命令: ``` sudo systemctl start janus ``` 4. 在浏览器中查看视频流 打开浏览器,输入 Janus Gateway 的 URL(默认为 `http://localhost:8088/janus`),进入 Janus Gateway 的界面。在界面中选择 `Streaming`,然后选择 `Play`,填入以下参数: - Type:选择 `WebRTC` - Video room:选择 `1234` - Video codec:选择 `VP8` - Video bitrate:选择 `512000` - Audio codec:选择 `opus` 然后点击 `Start`,就可以在浏览器中观看视频流了。 5. 推流服务器 最后一步是将视频流推送到服务器。可以使用 WebRTC 的 RTCDataChannel 将视频流传输到服务器。具体实现方法因服务器不同而异,可以参考 WebRTC 相关的文档和教程。 以上就是将树莓派摄像头RTMP 协议转换为 WebRTC 协议并推流服务器的步骤。 ### 回答2: 在树莓派中将摄像头RTMP协议转换为WebRTC协议并推流服务器的方法如下: 1. 首先,我们需要安装和配置WebRTC技术栈。可以使用开源的WebRTC库或框架,例如WebRTC.org提供的WebRTC库。安装和配置这些库可能需要一些基础的Linux命令和编译技能。 2. 配置树莓派摄像头并连接到树莓派。摄像头可以通过CSI接口或USB连接到树莓派。 3. 编写一个小程序来捕获摄像头视频流并将其转换为WebRTC协议。这个程序可以使用Python编写,并使用相应的库或框架来实现WebRTC协议。 4. 使用WebRTC技术将视频流推送到服务器。首先,需要通过WebRTC建立一个信令通道来交换媒体数据的SDP(Session Description Protocol)。然后,使用SDP来建立点对点连接,并将视频流通过WebRTC协议推送到服务器。 5. 在服务器端配置一个WebRTC服务器,用于接收来自树莓派的视频流并进行处理。可以使用开源的WebRTC媒体服务器,例如Kurento Media Server或Janus Gateway等。 6. 在WebRTC服务器上配置流媒体服务器,例如NGINX或Wowza,以便将视频流推送到其他设备或应用程序。 总结起来,将摄像头RTMP协议转换为WebRTC协议并推流服务器的步骤包括安装和配置WebRTC技术栈、配置树莓派摄像头、编写捕获和转换视频流的程序、建立WebRTC信令通道、推送视频流到服务器,并在服务器上配置流媒体服务器。 ### 回答3: 在树莓派上,将摄像头RTMP协议转成WebRTC协议并推流服务器,可以通过以下步骤实现: 首先,需要安装并配置相应的软件和组件。在树莓派上安装FFmpeg来处理视频流,并配置好摄像头。 其次,需要利用WebRTC的技术来进行协议转换。WebRTC是一种支持实时音视频通信的开源协议,需要在树莓派上安装WebRTC相关的库和组件。 接下来,通过编写代码和脚本来实现协议转换和推流。可以使用Python等编程语言来编写代码,调用FFmpeg和WebRTC相关的命令和函数来处理视频流,并将RTMP协议转成WebRTC协议。 在代码中,可以设置树莓派作为WebRTC的发送端,将摄像头捕获的视频流进行编码和转换,然后通过WebRTC协议将其推流服务器。可以使用WebRTC提供的API和函数,例如`getUserMedia`函数来捕获摄像头视频流,`createOffer`函数来生成WebRTC的offer,并通过WebSocket或其他方式将其发送到服务器。 最后,在服务器上也需要相应的配置和组件来接收和处理WebRTC协议的推流。可以使用WebRTC提供的API和库来接收和处理树莓派推流过来的视频流,并进行解码和显示。 综上所述,需要安装配置FFmpeg和WebRTC相关的组件,编写代码实现协议转换和推流,并在服务器上进行相应的配置和处理,才能实现在树莓派中将摄像头RTMP协议转成WebRTC协议并推流服务器的功能。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值