- SSL Pinning & Signature checking(SecureCoding – P2)
- Review Source code
- Giao thức bảo mật HTTPS và MITM attack(Secure Coding P1)
- Tại sao nên hạn chế sử dụng Singleton, static function(util class, Helper class)?
- The Good, The Bad and the Ugly
- Media Player (Part2) – Add Record function vào IJKPlayer trên Android
- Media Player (Part1) – RTSP Player Android
- Raw String in Swift
- If / switch expression – Swift 5.9 (P.1)
Đ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:
/* 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:
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)
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
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
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:
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
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