首先这个项目并未实现音频的传输,后面有时间再实现音频的传输后更新博文。这里如果是自己部署流媒体服务器,可以参考搭建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)
{
}
//