Ijkplayer实现录像和截图功能

怎么编译Ijkplayer 这里就不说了,自行百度吧

​ 截图、录制的实现是根据某个变量条件判断的,视频在播放过程中不断循环获取数据进行解封装、解压缩,在这过程中,根据一个变量判断是否需要截图或录制视频;截图 的变量理应是一次性的,完成截图功能之后,把对应截图变量变成假;录制则根据用户停止或者读取到文件结尾后停止录制

1、截图实现原理是在播放画面之前,判断是否需要截图,是则拿到 AVFrame对其进行编码,然后封装成一个图片文件

2、录制也是一样的原理,在解码AVPacket 之前判断是否需要录像,是则复制一份AVPacket ,然后封装成音视频文件

一、配置编译参数

向config 目录下的module-lite.sh 文件末尾添加以下内容:

export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-protocol=rtp"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=rtsp"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-muxer=mjpeg"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=mjpeg"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=mjpeg"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-encoder=mjpeg"

(我项目中播放的是rtsp的,所以ffmpeg 需要开启rtsp支持,还有截图功能)

然后删除 module.sh文件,重新生成module.sh 文件

ln -s module-lite.sh module.sh

( ln -s 相当于重定向 )

然后重新编译ffmpeg库,在 ijkplayer-android / android / contrib 目录下:

./compile-ffmpeg.sh clean  //清理以前编译的ffmpeg库文件
./compile-ffmpeg.sh armv7a //编译armv7a 架构的so库

二、添加代码

以下所有操作的文件都在 ijkplayer-android/ijkmedia/ijkplayer 目录中

1、核心方法与变量

首先添加一些变量

向 ff_ffplay_def.h 头文件中的 FFPlayer 结构体添加以下变量

	int is_screenshot;//是否截图,是一次性的,一次只能截一张图
    char *screen_file_name;//输出图片的路径 screen_file_name;//截图保存地址
	
	AVFormatContext *m_ofmt_ctx;        // 用于输出的AVFormatContext结构体
    AVOutputFormat *m_ofmt;
    pthread_mutex_t record_mutex;       // 锁
    int is_record;                      // 是否在录制
    int record_error;  //虽然没用,但是代码中使用到了,所以别删            
    
	int videoindex;//视频输入流的下标
	int audeoindex;//音频输入流的下标

	int videoindex_out;//视频输出流的下标
	int audioindex_out;//音频输出流的下标
	
	int record_count; //录制packet 数量,这个测试用的,虽然没用,但是代码中使用到了,所以别删            
    int is_first;     // 用于录制时候,当某一帧的 pts 和 dts 一致时,才开始录制
    int64_t start_pts;                  // 开始录制时pts
    int64_t start_dts;                  // 开始录制时dts

以下是FFPlayer 结构体的部分内容(方便对照)


typedef struct FFPlayer {
    const AVClass *av_class;

    /* ffplay context */
    VideoState *is;

    /* format/codec options */
    AVDictionary *format_opts;
    AVDictionary *codec_opts;
    AVDictionary *sws_dict;
    AVDictionary *player_opts;
    AVDictionary *swr_opts;
    AVDictionary *swr_preset_opts;

	int is_screenshot;//是否截图,是一次性的,一次只能截一张图
    char *screen_file_name;//输出图片的路径 screen_file_name;//截图保存地址
	
	AVFormatContext *m_ofmt_ctx;        // 用于输出的AVFormatContext结构体
    AVOutputFormat *m_ofmt;
    pthread_mutex_t record_mutex;       // 锁
    int is_record;                      // 是否在录制
    int record_error;  //虽然没用,但是代码中使用到了,所以别删            
    
	int videoindex;//视频输入流的下标
	int audeoindex;//音频输入流的下标

	int videoindex_out;//视频输出流的下标
	int audioindex_out;//音频输出流的下标
	
	int record_count; //录制packet 数量,这个测试用的,虽然没用,但是代码中使用到了,所以别删            
    int is_first;     // 用于录制时候,当某一帧的 pts 和 dts 一致时,才开始录制
    int64_t start_pts;                  // 开始录制时pts
    int64_t start_dts;                  // 开始录制时dts


    /* ffplay options specified by the user */
#ifdef FFP_MERGE
    AVInputFormat *file_iformat;
    .
    .
    .
}

ff_ffplay.h 头文件末尾添加以下 函数

/*
* 截图  
* @param out_file 保存截图文件的全路径,只需要文件路径就行,不需要提前生成文件
*/
int ffp_get_current_frame(FFPlayer* ffp,const char* out_file);
/*
* 开始录制
* @param file_name 保存录制文件的全路径,只需要文件路径就行,不需要提前生成文件
*/
int ffp_start_record(FFPlayer *ffp, const char *file_name);
/*
* 结束录制
*/
int ffp_stop_record(FFPlayer *ffp);
/*
* 把 packet 包写入文件中
*/
int ffp_record_file(FFPlayer *ffp, AVPacket *packet);

/*
* 判断是否在录制
* 
*/
int ffp_is_record(FFPlayer *ffp);

/*
* 保存图片
@param out_file 文件路径
*/
int save_png(AVFrame *picture, const char *out_file);

ff_ffplay.c 文件添加实现

int ffp_get_current_frame(FFPlayer* ffp,const char* out_file) {

	if (!ffp->is_screenshot){
		ffp->is_screenshot = 1;
	
		if (ffp->screen_file_name!=NULL) {
			free(ffp->screen_file_name);
			ffp->screen_file_name = NULL;
		}
		ffp->screen_file_name = (char*)malloc(sizeof(char)*strlen(out_file)+1);
		strcpy(ffp->screen_file_name, out_file);
		return 1;
	}
	return 0;
}
//开始录制函数:file_name是保存路径
int ffp_start_record(FFPlayer *ffp, const char *file_name)
{
	J4A_ALOGE("开始录制函数:%s\n",file_name);
    assert(ffp);
    
    VideoState *is = ffp->is;
	
	int ret;
    
    ffp->m_ofmt_ctx = NULL;
    ffp->m_ofmt = NULL;
    ffp->is_record = 0;
    ffp->record_error = 0;
	
	
	ffp->videoindex = -1;
	ffp->audeoindex = -1;
	ffp->videoindex_out = -1;//视频输出流的下标
	ffp->audioindex_out = -1;//音频输出流的下标
    
    if (!file_name || !strlen(file_name)) { // 没有路径
        J4A_ALOGE("filename is invalid\n");
        goto end;
    }
    
    if (!is || !is->ic|| is->paused || is->abort_request) { // 没有上下文,或者上下文已经停止
        J4A_ALOGE("没有上下文,或者上下文已经停止\n");
        goto end;
    }
    
    if (ffp->is_record) { // 已经在录制
        
		J4A_ALOGE("已经在录制\n");
        goto end;
    }
	
	for (int i = 0; i < is->ic->nb_streams; i++) {
		AVStream* av_stream = is->ic->streams[i];
		enum AVMediaType mediaType = av_stream->codecpar->codec_type;

		if (mediaType == AVMEDIA_TYPE_VIDEO) {
			ffp->videoindex = i;
			J4A_ALOGE("视频流= %d\n", i);//1

		}else if (mediaType == AVMEDIA_TYPE_AUDIO) {
			ffp->audeoindex = i;
			J4A_ALOGE("音频流= %d\n", i);//2
		}
		else
		{
			continue;
		}
	}
    
    // 初始化一个用于输出的AVFormatContext结构体
    avformat_alloc_output_context2(&ffp->m_ofmt_ctx, NULL, NULL, file_name);
    if (!ffp->m_ofmt_ctx) {
        av_log(ffp, AV_LOG_ERROR, "Could not create output context filename is %s\n", file_name);
		J4A_ALOGE("创建输出的AVFormatContext结构体失败-->%s",file_name);
        goto end;
    }
    ffp->m_ofmt = ffp->m_ofmt_ctx->oformat;
   
    for (int i = 0; i < is->ic->nb_streams; i++) {
      if (i==ffp->videoindex || i== ffp->audeoindex ){
			AVStream* in_stream = is->ic->streams[i];
			AVCodecParameters* codecpar = in_stream->codecpar;
			enum AVMediaType mediaType = codecpar->codec_type;

			AVCodec* dec = avcodec_find_decoder(codecpar->codec_id);
			AVStream* out_stream = avformat_new_stream(ffp->m_ofmt_ctx, dec);
			if (!out_stream) {
				J4A_ALOGE("Failed allocating output stream\n");
				ret = AVERROR_UNKNOWN;
				goto end;
			}
			if (i == ffp->videoindex) {
				ffp->videoindex_out = out_stream->index;
			}
			else if (i == ffp->audeoindex){
				ffp->audioindex_out = out_stream->index;
			}else {
			
				
			}
			codecpar->codec_tag = 0;
			AVCodecContext* context = avcodec_alloc_context3(dec);
			ret = avcodec_parameters_to_context(context, codecpar);
			//ret = avcodec_parameters_from_context(codecpar, context);
			//out_stream->codecpar = codecpar;
			out_stream->codec = context;

		}else{
			continue;
		}
    }
    
    av_dump_format(ffp->m_ofmt_ctx, 0, file_name, 1);
    
    // 打开输出文件
    if (!(ffp->m_ofmt->flags & AVFMT_NOFILE)) {
        if (avio_open(&ffp->m_ofmt_ctx->pb, file_name, AVIO_FLAG_WRITE) < 0) {
            av_log(ffp, AV_LOG_ERROR, "Could not open output file '%s'", file_name);
            goto end;
        }
    }
    
    // 写视频文件头
    if (avformat_write_header(ffp->m_ofmt_ctx, NULL) < 0) {
        av_log(ffp, AV_LOG_ERROR, "Error occurred when opening output file\n");
        goto end;
    }
    
    ffp->is_record = 1;
    ffp->record_error = 0;
    pthread_mutex_init(&ffp->record_mutex, NULL);
    
    return 0;
end:
    ffp->record_error = 1;
    return -1;
}


//停止录播
int ffp_stop_record(FFPlayer *ffp)
{
   assert(ffp);
	if (ffp->is_record) {
		ffp->is_record = 0;
		pthread_mutex_lock(&ffp->record_mutex);
		if (ffp->m_ofmt_ctx != NULL) {
			av_write_trailer(ffp->m_ofmt_ctx);
			if (ffp->m_ofmt_ctx && !(ffp->m_ofmt->flags & AVFMT_NOFILE)) {
				avio_close(ffp->m_ofmt_ctx->pb);
			}
			avformat_free_context(ffp->m_ofmt_ctx);
			ffp->m_ofmt_ctx = NULL;
			ffp->is_first = 0;
			ffp->record_count = 0;
	

			ffp->videoindex = -1;
			ffp->audeoindex = -1;
			ffp->videoindex_out = -1;
			ffp->audioindex_out = -1;

			ffp->start_pts = 0;
			ffp->start_dts = 0;
		
		}
		pthread_mutex_unlock(&ffp->record_mutex);
		pthread_mutex_destroy(&ffp->record_mutex);
		av_log(ffp, AV_LOG_DEBUG, "stopRecord ok\n");
	}
	else {
		av_log(ffp, AV_LOG_ERROR, "don't need stopRecord\n");
	}
	return 0;
}


int ffp_record_file(FFPlayer* ffp, AVPacket* packet)
{
	assert(ffp);
	VideoState* is = ffp->is;
	int ret = 0;
	int stream_index = 0;
	AVStream* in_stream;
	AVStream* out_stream;

	int pkt_index = packet->stream_index;

	if (pkt_index == ffp->videoindex || pkt_index == ffp->audeoindex) {

		if (ffp->is_record) {
			if (packet == NULL) {
				ffp->record_error = 1;
				av_log(ffp, AV_LOG_ERROR, "packet == NULL");
				return -1;
			}

			AVPacket* pkt = (AVPacket*)av_malloc(sizeof(AVPacket)); // 与看直播的 AVPacket分开,不然卡屏
			av_new_packet(pkt, 0);
			if (0 == av_packet_ref(pkt, packet)) {
				pthread_mutex_lock(&ffp->record_mutex);

				// 需要减去第一帧的时间,
				pkt->pts =pkt->pts - ffp->start_pts;
				pkt->dts =pkt->dts - ffp->start_dts;

				in_stream = is->ic->streams[pkt->stream_index];

				if (pkt->stream_index == ffp->videoindex) {
					out_stream = ffp->m_ofmt_ctx->streams[ffp->videoindex_out];
					stream_index=ffp->videoindex_out;
				}
				else {
					out_stream = ffp->m_ofmt_ctx->streams[ffp->audioindex_out];
					stream_index=ffp->audioindex_out;
				}

				// 转换PTS/DTS
				pkt->pts = av_rescale_q_rnd(pkt->pts, in_stream->time_base, out_stream->time_base, (AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
				pkt->dts = av_rescale_q_rnd(pkt->dts, in_stream->time_base, out_stream->time_base, (AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
				pkt->duration = av_rescale_q(pkt->duration, in_stream->time_base, out_stream->time_base);
				pkt->pos = -1;
				pkt->stream_index = stream_index;
				
				ffp->record_count++;
				
				J4A_ALOGE("录制packet数量=%d\n", ffp->record_count);

				// 写入一个AVPacket到输出文件
				if ((ret = av_interleaved_write_frame(ffp->m_ofmt_ctx, pkt)) < 0) {
					av_log(ffp, AV_LOG_ERROR, "Error muxing packet\n");
				}
				
				 

				av_packet_unref(pkt);
				pthread_mutex_unlock(&ffp->record_mutex);
			}
			else {
				av_log(ffp, AV_LOG_ERROR, "av_packet_ref == NULL");
			}
		}
	}
	return ret;
}

//是否正在录制
int ffp_is_record(FFPlayer *ffp){
	return ffp->is_record;
}
//保存图片
int save_png(AVFrame *picture, const char *out_file) {
    AVFormatContext *pFormatCtx;
    AVOutputFormat *fmt;
    AVStream *video_st;
    AVCodecContext *pCodecCtx;
    AVCodec *pCodec;

    AVPacket pkt;
    int y_size;
    int got_picture = 0;

    int ret = 0;


    //Method 1
    pFormatCtx = avformat_alloc_context();
    //Guess format
    fmt = av_guess_format("mjpeg", NULL, NULL);
    pFormatCtx->oformat = fmt;
    //Output URL
    if (avio_open(&pFormatCtx->pb, out_file, AVIO_FLAG_READ_WRITE) < 0) {
        avformat_free_context(pFormatCtx);
        J4A_ALOGE("Couldn't open output file.");
        return -1;
    }

    //Method 2. More simple
    //avformat_alloc_output_context2(&pFormatCtx, NULL, NULL, out_file);
    //fmt = pFormatCtx->oformat;

    video_st = avformat_new_stream(pFormatCtx, 0);
    if (video_st == NULL) {
        avio_close(pFormatCtx->pb);
        avformat_free_context(pFormatCtx);
        return -1;
    }
    pCodecCtx = video_st->codec;
    pCodecCtx->codec_id = fmt->video_codec;
    pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
    pCodecCtx->pix_fmt = AV_PIX_FMT_YUVJ420P;

    pCodecCtx->width = picture->width;
    pCodecCtx->height = picture->height;

    pCodecCtx->time_base.num = 1;
    pCodecCtx->time_base.den = 25;
    //Output some information
    av_dump_format(pFormatCtx, 0, out_file, 1);
    AV_CODEC_ID_MJPEG;
    pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
    if (!pCodec) {
        avcodec_close(video_st->codec);
        avio_close(pFormatCtx->pb);
        avformat_free_context(pFormatCtx);
        J4A_ALOGE("Codec not found.");
        return -1;
    }
    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
        J4A_ALOGE("Could not open codec.");
        avcodec_close(video_st->codec);
        avio_close(pFormatCtx->pb);
        avformat_free_context(pFormatCtx);
        return -1;
    }

    //Write Header
    avformat_write_header(pFormatCtx, NULL);

    y_size = pCodecCtx->width * pCodecCtx->height;
    av_new_packet(&pkt, y_size * 3);

    ret = avcodec_encode_video2(pCodecCtx, &pkt, picture, &got_picture);
    if (ret < 0) {
        J4A_ALOGE("Encode Error.\n");
        avcodec_close(video_st->codec);
        avio_close(pFormatCtx->pb);
        avformat_free_context(pFormatCtx);
        return -1;
    }
    if (got_picture == 1) {
        pkt.stream_index = video_st->index;
        ret = av_write_frame(pFormatCtx, &pkt);
    }

    av_free_packet(&pkt);
    //Write Trailer
    av_write_trailer(pFormatCtx);

    J4A_ALOGE("Encode Successful.\n");

    if (video_st) {
        avcodec_close(video_st->codec);
    }
    avio_close(pFormatCtx->pb);
    avformat_free_context(pFormatCtx);

    return 0;

}

以上代码使用了Android NDK的打印方法,所以在ff_ffplay.h 加入头文件

#include "ff_ffplay_def.h"
#include "ff_fferror.h"
#include "ff_ffmsg.h"

#include "j4a/j4a_base.h" //加这个

以上是基础函数的声明和实现,真正的核心功能,现在需要在什么位置调用相应的函数:

2、截图插入位置

​ 理想的截图位置是在显示画面之前拿到AVFrame 帧,然后封装成一张图片,但是ijkplayer 内部把AVFrame 转成了一个SDL_VoutOverlay ,数据发生改变,死活封装不成一张图片(没研究透),所以只能在转化成 SDL_VoutOverlay 之前判断 是否需要截图,好在也能截取当前画面,视觉误差几乎没有;

​ 截图插入位置,在 ff_ffplay.c 中的 ffplay_video_thread() 函数中插入以下代码

if (ffp->is_screenshot) {
	J4A_ALOGE("截图保存地址=%s", ffp->screen_file_name);
	ffp->is_screenshot=0;
	save_png(frame, ffp->screen_file_name);
	free(ffp->screen_file_name);
	ffp->screen_file_name = NULL;
}

ffplay_video_thread 部分代码

/*视频解码函数*/
static int ffplay_video_thread(void* arg)
{
	FFPlayer* ffp = arg;
	VideoState* is = ffp->is;
	AVFrame* frame = av_frame_alloc();
	double pts;
	double duration;
	int ret;
	AVRational tb = is->video_st->time_base;
	AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
	int64_t dst_pts = -1;
	int64_t last_dst_pts = -1;
	int retry_convert_image = 0;
	int convert_frame_count = 0;


	for (;;) {
                .
                .
                .  n行代码
                .
                .
		
			duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational) { frame_rate.den, frame_rate.num }) : 0);
			pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
        
        	//截图插入位置,直接复制这段代码即可
        	if (ffp->is_screenshot) {
				J4A_ALOGE("截图保存地址=%s", ffp->screen_file_name);
				ffp->is_screenshot=0;
				save_png(frame, ffp->screen_file_name);
				free(ffp->screen_file_name);
				ffp->screen_file_name = NULL;
			}
        
        
			ret = queue_picture(ffp, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
        	// 好像也可以放到这个位置,还不太清楚 queue_picture 函数中有没有对frame内容做修改,所以放在 queue_picture 之前
			av_frame_unref(frame);
            .
            .
            . 若干行代码
            .
            .
the_end:
#if CONFIG_AVFILTER
	avfilter_graph_free(&graph);
#endif
	av_log(NULL, AV_LOG_INFO, "convert image convert_frame_count = %d\n", convert_frame_count);
	av_frame_free(&frame);
	return 0;
}

3、录制插入位置

​ 理想的录制流程是,拿到播放前的音频、视频数据,重新编码,在把音频、视频数据封装成 音视频文件,但是视频帧经过转化成了SDL_VoutOverlay ,所以这种方法放弃了(难度也比较大);

​ 所以这里的录制时,在AVPacket 解码成 AVFrame 之前,判断是否录制;刚好ijkplayer 音视频数据解码 都在decoder_decode_frame 函数中进行,在函数decoder_decode_frame中插入以下代码:

// 获取开始录制前dts等于pts最后的值,这样录像第一帧的的显示时间和解码时间一致
if (!ffp->is_first && pkt.pts == pkt.dts) {
	ffp->is_first = 1;
	ffp->start_pts = pkt.pts;
	ffp->start_dts = pkt.dts;
}

if (ffp->is_record && ffp->is_first) { // 可以录制时,写入文件
	if (0 != ffp_record_file(ffp, &pkt)) {
		ffp->record_error = 1;
		ffp_stop_record(ffp);
	}
}

decoder_decode_frame 部分代码:

static int decoder_decode_frame(FFPlayer *ffp, Decoder *d, AVFrame *frame, AVSubtitle *sub) {
    int ret = AVERROR(EAGAIN);
	
    for (;;) {
        AVPacket pkt;
            .
            .
            . 若干代码
            .
            .
				int result = avcodec_send_packet(d->avctx, &pkt);
				//录制插入代码,开始
				// 获取开始录制前dts等于pts最后的值,这样录像第一帧的的显示时间和解码时间一致
				if (!ffp->is_first && pkt.pts == pkt.dts) { 
					ffp->is_first = 1;
					ffp->start_pts = pkt.pts;
					ffp->start_dts = pkt.dts;
				}

				if (ffp->is_record && ffp->is_first) { // 可以录制时,写入文件
					if (0 != ffp_record_file(ffp, &pkt)) {
						ffp->record_error = 1;
						J4A_ALOGE("意外停止录制");
						ffp_stop_record(ffp);
					}
				}
        
				//录制插入代码,结束
				
                if (result == AVERROR(EAGAIN)) {
                    av_log(d->avctx, AV_LOG_ERROR, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
                    d->packet_pending = 1;
                    av_packet_move_ref(&d->pkt, &pkt);
                }
            }
            av_packet_unref(&pkt);
        }
    }
}

-------------------------------------------------分割线---------------------------------------------------

下面的代码只是调用上面的代码而已:

4、Android 调用ff_ffplay

ijkplayer 的内容只是就像个中介一样,Android 调用C 代码是在ijkplayer_jni 中,ijkplayer_jni 调用 具体功能只能通过ijkplayer 来调用;

向ijkplayer.h 头文件结尾添加函数声明, 也是对应ff_ffplay.h添加的函数,少了ffp_record_file函数,因为ffp_record_file函数是录制过程写入文件操作,外部不需要关心

int ijkmp_get_current_frame(IjkMediaPlayer* mp,const char* out_file);

int ijkmp_start_record(IjkMediaPlayer *mp,const char *file_name);

int ijkmp_stop_record(IjkMediaPlayer *mp);

int ijkmp_is_record(IjkMediaPlayer *mp);

ijkplayer.c 实现


int ijkmp_get_current_frame(IjkMediaPlayer* mp,const char* file_name) {
	assert(mp);
	MPTRACE("ijkmp_get_current_frament()\n");
	pthread_mutex_lock(&mp->mutex);
	int retval = ffp_get_current_frame(mp->ffplayer, file_name);
	pthread_mutex_unlock(&mp->mutex);
	MPTRACE("ijkmp_get_current_frament()=%d\n", retval);
	return retval;
}

int ijkmp_start_record(IjkMediaPlayer *mp,const char *file_name)
{
    assert(mp);
    MPTRACE("ijkmp_startRecord()\n");
    pthread_mutex_lock(&mp->mutex);
    int retval = ffp_start_record(mp->ffplayer,file_name);
    pthread_mutex_unlock(&mp->mutex);
    MPTRACE("ijkmp_startRecord()=%d\n", retval);
    return retval;
}

int ijkmp_stop_record(IjkMediaPlayer *mp)
{
    assert(mp);
    MPTRACE("ijkmp_stopRecord()\n");
    pthread_mutex_lock(&mp->mutex);
    int retval = ffp_stop_record(mp->ffplayer);
    pthread_mutex_unlock(&mp->mutex);
    MPTRACE("ijkmp_stopRecord()=%d\n", retval);
    return retval;
}
int ijkmp_is_record(IjkMediaPlayer *mp)
{
    assert(mp);
    MPTRACE("ijkmp_stopRecord()\n");
    pthread_mutex_lock(&mp->mutex);
    int retval = ffp_is_record(mp->ffplayer);
    pthread_mutex_unlock(&mp->mutex);
    MPTRACE("ijkmp_stopRecord()=%d\n", retval);
    return retval;
}

以下是Android 调用 C 代码的方法映射,在 …/ijkplayer-android/ijkmedia/ijkplayer/android 目录下

向ijkplayer_jni.c 中加入方法映射 (注意看注释,不要直接复制)

 static JNINativeMethod g_methods[] = {
    {
        "_setDataSource",
        "(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;)V",
        (void *) IjkMediaPlayer_setDataSourceAndHeaders
    },
    { "_setDataSourceFd",       "(I)V",     (void *) IjkMediaPlayer_setDataSourceFd },
    { "_setDataSource",         "(Ltv/danmaku/ijk/media/player/misc/IMediaDataSource;)V", (void *)IjkMediaPlayer_setDataSourceCallback },
    { "_setAndroidIOCallback",  "(Ltv/danmaku/ijk/media/player/misc/IAndroidIO;)V", (void *)IjkMediaPlayer_setAndroidIOCallback },
         .
         . 省略
         .
	
     //添加下面这几个
    { "getCurrentFrame",        "(Ljava/lang/String;)I", (void *) IjkMediaPlayer_getCurrentFrame },
	{ "startRecord",            "(Ljava/lang/String;)I",      (void *) IjkMediaPlayer_startRecord },
    { "stopRecord",             "()I",      (void *) IjkMediaPlayer_stopRecord },
    { "isRecord",             "()I",      (void *) IjkMediaPlayer_isRecord },
	
};

对应的本地映射方法

static int
IjkMediaPlayer_getCurrentFrame(JNIEnv *env, jclass thiz, jstring name)
{
    const char *c_name = (*env)->GetStringUTFChars(env, name, 0);
    jint retval = 0;
    IjkMediaPlayer* mp = jni_get_media_player(env, thiz);
    JNI_CHECK_GOTO(mp, env, NULL, "mpjni: IjkMediaPlayer_getCurrentFrame: null mp", LABEL_RETURN);
    retval = ijkmp_get_current_frame(mp,c_name);

LABEL_RETURN:
    ijkmp_dec_ref_p(&mp);
    if (c_name)
        (*env)->ReleaseStringUTFChars(env, name, c_name);
    return retval;
}

static jint
IjkMediaPlayer_startRecord(JNIEnv *env, jclass thiz,jstring file)
{
    jint retval = 0;
    IjkMediaPlayer *mp = jni_get_media_player(env, thiz);
    JNI_CHECK_GOTO(mp, env, NULL, "mpjni: startRecord: null mp", LABEL_RETURN);
    const char *nativeString = (*env)->GetStringUTFChars(env, file, 0);
    retval = ijkmp_start_record(mp,nativeString);

LABEL_RETURN:
    ijkmp_dec_ref_p(&mp);
    return retval;
}

static jint
IjkMediaPlayer_stopRecord(JNIEnv *env, jclass thiz)
{
    jint retval = 0;
    IjkMediaPlayer *mp = jni_get_media_player(env, thiz);
    JNI_CHECK_GOTO(mp, env, NULL, "mpjni: stopRecord: null mp", LABEL_RETURN);

    retval = ijkmp_stop_record(mp);

LABEL_RETURN:
    ijkmp_dec_ref_p(&mp);
    return retval;
}
static jint
IjkMediaPlayer_isRecord(JNIEnv *env, jclass thiz)
{
    jint retval = 0;
    IjkMediaPlayer *mp = jni_get_media_player(env, thiz);
    JNI_CHECK_GOTO(mp, env, NULL, "mpjni: stopRecord: null mp", LABEL_RETURN);

    retval = ijkmp_is_record(mp);

LABEL_RETURN:
    ijkmp_dec_ref_p(&mp);
    return retval;
}

完成之后编译生成 生成库文件,在/ijkplayer-android/android 目录下执行以下命令

./compile-ijk.sh clean  //清理以前生成的库文件
./compile-ijk.sh armv7a //编译生成armv7a 结构的ijkplayer播放器的动态库文件

三、Android 使用

编译好的so库在 …/ijkplayer-android/android/ijkplayer/cpu架构/src/main/libs / 对应的upc架构 中,项目中添加so库,

因为添加了jni 的映射方法,所以也需要添加Android 层的方法调用到jni。导入 ijkplayer-java 模块,调用jni 层的代码是在 IjkMediaPlayer 类中,向IjkMediaPlayer添加以下native 方法

@Override
public native int startRecord(String recordVideoPath);

@Override
public native int stopRecord();

// 如果是在录像中返回的值是 1 
@Override
public native int isRecord();

@Override
public native int getCurrentFrame(String saveFile);

(我项目中使用到了Ijkplayer 提供的IjkVideoView 播放控件,这个播放控件播放内核不止一个,所以我把以上方法放到了 IMediaPlayer 抽象类中,不使用Ijkplayer 提供的IjkVideoView 控件,直接使用 IjkMediaPlayer 来控制播放则在IjkMediaPlayer 中直接添加上面方法就好)

IjkVideoView 类中添加以下代码

public int startRecord(String recordVideoPath) {
    return mMediaPlayer.startRecord(recordVideoPath);
}

public int stopRecord() {
    return mMediaPlayer.stopRecord();
}

public boolean isRecord() {
     // 如果是在录像中返回的值是 1 
    return mMediaPlayer.isRecord() > 0;
}

public void getTestBitmp(String saveFile) {
    mMediaPlayer.getCurrentFrame(saveFile);
}

如果播放内核不是ijkplayer 判断一下 mMediaPlayer

因为我是录制的是直播,所以配置了一下内容

 ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 0);
        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "http-detect-range-support", 0);
        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48);

        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp");
        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp");

  //      ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video"); //根据媒体类型来配置
        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 20000);
        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316);
        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "infbuf", 1);  // 无限读
        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100L);
        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240L);
        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1L);
        //  关闭播放器缓冲,这个必须关闭,否则会出现播放一段时间后,一直卡主,控制台打印 FFP_MSG_BUFFERING_START
        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0L);
        ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 2L);

最后,不给demo说不过去,这里献上

参考:
iOS直播内容保存为本地视频

  • 9
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 52
    评论
ijkplayer 的动态库v7a 和v8a,支持海康摄像头RTSP h264协议,源码来自最新版本ijkplayer k0.8.0 使用原DEMO即可支持RTSP mp4文件等播放,首画面500ms显示. module-lite-rtsp.sh的内容如下: #! /usr/bin/env bash #-------------------- # Standard options: export COMMON_FF_CFG_FLAGS= # export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --prefix=PREFIX" # Licensing options: export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-gpl" # export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-version3" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-nonfree" # Configuration options: # export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-static" # export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-shared" # export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-small" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-runtime-cpudetect" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-gray" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-swscale-alpha" # Program options: export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-programs" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-ffmpeg" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-ffplay" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-ffprobe" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-ffserver" # Documentation options: export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-doc" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-htmlpages" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-manpages" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-podpages" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-txtpages" # Component options: export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-avdevice" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-avcodec" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-avformat" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-avutil" export COMMON_FF_CF
评论 52
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值