怎么编译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直播内容保存为本地视频