Media Player (Part2) – Add Record function vào IJKPlayer trên Android

by baka3k
311 views

Điểm mạnh của IJKPlayer là low latency, nó có độ trễ khá thấp khi streaming, nhưng giả sử phát sinh tình huống cần record một đoạn video khi đang streaming thì phải làm thế nào? IJKPlayer không support sẵn.

Sau khi tham khảo 1 số blog của các bạn…..Trung Quốc và build được thành công thì mình share lại thông tin cho ai cần(vâng, ko hiểu tại sao cứ liên quan đến video, camera,Streaming thì tài liệu chỉ có thể tìm thấy bên….Trung Quốc. Không phủ nhận, các bạn ý giỏi thật  )

Trước hết, các bạn cần build được IJKPlayer cho Android đã nhé, cụ thể có thể xem link bên dưới

OK, Sau khi các bạn biết cách chuẩn bị môi trường và build thành công, chúng ta cần tiến hành chỉnh sửa một chút trong thư viện IJKplayer(Mình sẽ không đề cập đến vấn đề pháp lý, licence ở đây)

Chúng ta cần check out source code và build thử nhé

git clone https://github.com/baka3k/IjkPlayerRecorder.git
cd IjkPlayerRecorder
// Khởi tạo project build cho Android
./init-android.sh
//IJKPlayer có sử dụng ffmpeg - nên việc build ffmpeg là bắt buộc
cd android/contrib
./compile-ffmpeg.sh clean
./compile-ffmpeg.sh all

//quay lại thư mục IJK và build IJKPlayer
cd ..
./compile-ijk.sh all

Nếu thành công – tức là bạn đã setup đầy đủ, có thể build IJKPlayer được, sẵn sàng cho việc thêm code để thêm chức năng recorder cho IJKPlayer.

Tiếp tục nhé

1.Trong thư mục ijkmedia/ijkplayer bạn tìm đến file ff_ffplay.h và khai báo các hàm sau:

Refer:

https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/ff_ffplay.h
/* record rtsp streaming */
int ffp_start_recording_l(FFPlayer *ffp, const char *file_name);
int ffp_record_isfinished_l(FFPlayer *ffp);
int ffp_stop_recording_l(FFPlayer *ffp);
void ffp_get_current_frame_l(FFPlayer *ffp, uint8_t *frame_buf);

2.Trong file ff_ffplay.c chúng ta định nghĩa nội dung thân hàm cho 4 function này

Refer:

https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/ff_ffplay.c
int ffp_start_recording_l(FFPlayer *ffp, const char *file_name)
{
assert(ffp);
VideoState *is = ffp->is;

ffp->m_ofmt_ctx = NULL;
ffp->m_ofmt = NULL;
ffp->is_record = 0;
ffp->record_error = 0;

if (!file_name || !strlen(file_name)) {
av_log(ffp, AV_LOG_ERROR, "filename is invalid");
goto end;
}

if (!is || !is->ic || is->paused || is->abort_request) {
av_log(ffp, AV_LOG_ERROR, "is,is->ic,is->paused is invalid");
goto end;
}

if (ffp->is_record) {
av_log(ffp, AV_LOG_ERROR, "recording has started");
goto end;
}

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);
goto end;
}
ffp->m_ofmt = ffp->m_ofmt_ctx->oformat;

for (int i = 0; i < is->ic->nb_streams; i++) {
AVStream *in_stream = is->ic->streams[i];
AVStream *out_stream = avformat_new_stream(ffp->m_ofmt_ctx, in_stream->codec->codec);
if (!out_stream) {
av_log(ffp, AV_LOG_ERROR, "Failed allocating output stream\n");
goto end;
}

av_log(ffp, AV_LOG_DEBUG, "in_stream->codec;%p\n", in_stream->codec);
if (avcodec_copy_context(out_stream->codec, in_stream->codec) < 0) {
av_log(ffp, AV_LOG_ERROR, "Failed to copy context from input to output stream codec context\n");
goto end;
}

out_stream->codec->codec_tag = 0;
if (ffp->m_ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) {
out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
}
}

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_record_isfinished_l(FFPlayer *ffp)
{
return 0;
}

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

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));
av_new_packet(pkt, 0);
if (0 == av_packet_ref(pkt, packet)) {
pthread_mutex_lock(&ffp->record_mutex);
if (!ffp->is_first) {
ffp->is_first = 1;
pkt->pts = 0;
pkt->dts = 0;
} else {
if (pkt->stream_index == AVMEDIA_TYPE_AUDIO) {
pkt->pts = llabs(pkt->pts - ffp->start_a_pts);
pkt->dts = llabs(pkt->dts - ffp->start_a_dts);
}
else if (pkt->stream_index == AVMEDIA_TYPE_VIDEO) {
pkt->pts = pkt->dts = llabs(pkt->dts - ffp->start_v_dts);
}
}

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

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;

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_stop_recording_l(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;
}
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;
}
void ffp_get_current_frame_l(FFPlayer *ffp, uint8_t *frame_buf)
{
ALOGD("=============>start snapshot\n");

VideoState *is = ffp->is;
Frame *vp;
int i = 0, linesize = 0, pixels = 0;
uint8_t *src;

vp = &is->pictq.queue[is->pictq.rindex];
int height = vp->bmp->h;
int width = vp->bmp->w;

ALOGD("=============>%d X %d === %d\n", width, height, vp->bmp->pitches[0]);

// copy data to bitmap in java code
linesize = vp->bmp->pitches[0];
src = vp->bmp->pixels[0];
pixels = width * 4;
for (i = 0; i < height; i++)
{
memcpy(frame_buf + i * pixels, src + i * linesize, pixels);
}
ALOGD("=============>end snapshot\n");
}

3. Define thêm các cấu trúc dữ liệu cần thiết trong ff_ffplay_def.h

Refer(line 724->733)

https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/ff_ffplay_def.h
AVFormatContext *m_ofmt_ctx;
AVOutputFormat *m_ofmt;
pthread_mutex_t record_mutex;
int is_record;
int record_error;
int is_first;
int64_t start_v_pts;
int64_t start_v_dts;
int64_t start_a_pts;
int64_t start_a_dts;

4. Define thêm hàm record trong ijkplayer.c(đây chính là class player mà chúng ta sẽ phải viết thêm JNI để câu xuống)

Define hàm vào header file ijkplayer.h

Refer

https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/ijkplayer.h
int ijkmp_start_recording(IjkMediaPlayer *mp, const char *filePath);
int ijkmp_stop_recording(IjkMediaPlayer *mp);
int ijkmp_isRecording(IjkMediaPlayer *mp);
void ijkmp_get_current_frame(IjkMediaPlayer *mp,uint8_t *frame_buf);

Viết thân hàm vào ijkplayer.c

Refer

https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/ijkplayer.c
static int ijkmp_start_recording_l(IjkMediaPlayer *mp, const char *filePath)
{
av_log(mp->ffplayer,AV_LOG_INFO,"cjz ijkmp_start_recording_l filePath %s",filePath);
return ffp_start_recording_l(mp->ffplayer, filePath);
}

int ijkmp_start_recording(IjkMediaPlayer *mp,const char *filePath)
{
assert(mp);
pthread_mutex_lock(&mp->mutex);
av_log(mp->ffplayer,AV_LOG_WARNING,"cjz ijkmp_start_recording --- ");
int retval = ijkmp_start_recording_l(mp,filePath);
printf("ijkmp_start_recording return == %d\n",retval);
pthread_mutex_unlock(&mp->mutex);
return retval;
}

static int ijkmp_stop_recording_l(IjkMediaPlayer *mp)
{
return ffp_stop_recording_l(mp->ffplayer);
}

int ijkmp_stop_recording(IjkMediaPlayer *mp)
{
assert(mp);
pthread_mutex_lock(&mp->mutex);
av_log(mp->ffplayer,AV_LOG_WARNING,"cjz ijkmp_stop_recording");
int retval = ijkmp_stop_recording_l(mp);
pthread_mutex_unlock(&mp->mutex);
return retval;
}

static int ijkmp_isRecordFinished_l(IjkMediaPlayer *mp)
{
return ffp_record_isfinished_l(mp->ffplayer);
}

int ijkmp_isRecordFinished(IjkMediaPlayer *mp)
{
assert(mp);
pthread_mutex_lock(&mp->mutex);
av_log(mp->ffplayer,AV_LOG_WARNING,"cjz ijkmp_isRecordFinished ");
int retval = ijkmp_isRecordFinished_l(mp);
pthread_mutex_unlock(&mp->mutex);
return retval;
}

int ijkmp_isRecording(IjkMediaPlayer *mp) {
return mp->ffplayer->is_record;
}
void ijkmp_get_current_frame(IjkMediaPlayer *mp,uint8_t *frame_buf){
ffp_get_current_frame_l(mp->ffplayer,frame_buf);
}

5. Cuối cùng, update thêm JNI để có thể call được từ tầng java xuống C nhé

Refer:

https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/android/ijkplayer_jni.c

Các bạn tìm đến đường dẫn IjkPlayerRecorder/ijkmedia/ijkplayer/android/ijkplayer_jni.c

Và thêm vào

static jint
IjkMediaPlayer_startRecord(JNIEnv *env, jobject 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_recording(mp,nativeString);

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

static jint
IjkMediaPlayer_stopRecord(JNIEnv *env, jobject 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_recording(mp);

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

static jboolean
IjkMediaPlayer_getCurrentFrame(JNIEnv *env, jobject thiz, jobject bitmap)
{
jboolean retval = JNI_TRUE;
IjkMediaPlayer *mp = jni_get_media_player(env, thiz);
JNI_CHECK_GOTO(mp, env, NULL, "mpjni: getCurrentFrame: null mp", LABEL_RETURN);

uint8_t *frame_buffer = NULL;

if (0 > AndroidBitmap_lockPixels(env, bitmap, (void **)&frame_buffer)) {
(*env)->ThrowNew(env, "java/io/IOException", "Unable to lock pixels.");
return JNI_FALSE;
}

ijkmp_get_current_frame(mp, frame_buffer);

if (0 > AndroidBitmap_unlockPixels(env, bitmap)) {
(*env)->ThrowNew(env, "java/io/IOException", "Unable to unlock pixels.");
return JNI_FALSE;
}

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

6. Chỉnh sửa các function để JNI map với java

Trong file ijkplayer_jni.c, tìm đến line 1190 có đoạn define

static JNINativeMethod g_methods[] = {....

Add thêm

{ "startRecord", "(Ljava/lang/String;)I", (void *) IjkMediaPlayer_startRecord },
{ "stopRecord", "()I", (void *) IjkMediaPlayer_stopRecord },
{ "getCurrentFrame", "(Landroid/graphics/Bitmap;)Z", (void *) IjkMediaPlayer_getCurrentFrame },

Refer

https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/android/ijkplayer_jni.c

Việc update code đến đây là OK

Chúng ta build lại IJKPlayer

cd android/contrib
./compile-ffmpeg.sh clean
./compile-ffmpeg.sh all

cd ..
./compile-ijk.sh all

Chờ đợi khá lâu đấy, nếu build lần đầu tiên có khả năng mất 1 tiếng :(, sau khi kết thúc build các bạn sẽ tìm thấy các file .so trong thư mục

- /IjkPlayerRecorder/android/ijkplayer/ijkplayer-armv7a/src/main/libs/armeabi-v7a
- /IjkPlayerRecorder/android/ijkplayer/ijkplayer-armv5/src/main/libs/armeabi
- /IjkPlayerRecorder/android/ijkplayer/ijkplayer-arm64/src/main/libs/arm64-v8a

Copy các file SO này vào project sample

IjkPlayerRecorder/android/ijkplayer/ijkplayer-example

và thử thôi

Series Navigation<< Media Player (Part1) – RTSP Player AndroidThe Good, The Bad and the Ugly >>

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.