bitmap类型字幕多见于蓝光片源。但是在原生ijkplayer中,只有针对文本类型字幕的处理,而不支持bitmap类型字幕,相关代码如下
//static void video_image_display2(FFPlayer *ffp) @ ff_ffplay.c
if (is->subtitle_st) {
if (frame_queue_nb_remaining(&is->subpq) > 0) {
sp = frame_queue_peek(&is->subpq);
if (vp->pts >= sp->pts + ((float) sp->sub.start_display_time / 1000)) {
if (!sp->uploaded) {
if (sp->sub.num_rects > 0) {
char buffered_text[4096];
if (sp->sub.rects[0]->text) { //在这里只对text类型和ass类型的字幕做了相应的的处理
strncpy(buffered_text, sp->sub.rects[0]->text, 4096);
}
else if (sp->sub.rects[0]->ass) {
parse_ass_subtitle(sp->sub.rects[0]->ass, buffered_text);
}
ffp_notify_msg4(ffp, FFP_MSG_TIMED_TEXT, 0, 0, buffered_text, sizeof(buffered_text));
}
sp->uploaded = 1;
}
}
}
}
当sp->sub.format==0时,对应的即为bitmap类型的字幕格式,如下定义
typedef struct AVSubtitle {
uint16_t format; /* 0 = graphics */
uint32_t start_display_time; /* relative to packet pts, in ms */
uint32_t end_display_time; /* relative to packet pts, in ms */
unsigned num_rects;
AVSubtitleRect **rects;
int64_t pts; ///< Same as packet pts, in AV_TIME_BASE
} AVSubtitle;
因此,要让ijkplayer支持bitmap类型字幕,需要做以下几件事情:
1.在video_image_display2方法中根据sp→sub.format
的值区分字幕类型,当是bitmap类型字幕时,取出相应的bmp像素数据
2.在jni层(ijkplayer_jni.c),利用拿到的bmp像素数据,调用android.graphics.Bitmap类中的方法,创建出相应的Bitmap对象
3.在java层(IjkMediaPlayer.java),拿到Bitmap对象,调用android.graphic.Canvas类的drawBitmap方法,绘制到单独的View中,从而显示出字幕的内容
下面来具体看一下每一步相关的代码
1.native层的修改,以PGS格式字幕为例
if (is->subtitle_st) {
if (frame_queue_nb_remaining(&is->subpq) > 0) {
sp = frame_queue_peek(&is->subpq);
if (vp->pts >= sp->pts + ((float) sp->sub.start_display_time / 1000)) {
if (!sp->uploaded) {
if (sp->sub.num_rects > 0) {
if (sp->sub.format != 0) { //此时字幕类型为文本类型,还是按照原有逻辑处理
char buffered_text[4096];
if (sp->sub.rects[0]->text) {
strncpy(buffered_text, sp->sub.rects[0]->text, 4096);
}
else if (sp->sub.rects[0]->ass) {
parse_ass_subtitle(sp->sub.rects[0]->ass, buffered_text);
}
ffp_notify_msg4(ffp, FFP_MSG_TIMED_TEXT, 0, 0, buffered_text, sizeof(buffered_text));
} else { //此时字幕类型为bitmap类型
if (sp->sub.rects[0]) {
if (sp->sub.rects[0]->data[0] && sp->sub.rects[0]->linesize[0] > 0) {
//w和h记录了bitmap字幕的宽和高。sp->sub.rects[0]还有成员x和y,记录的是bitmap字幕的坐标,因为我们想要的是可以调整字幕位置,所以不需要这两个变量。
int len = sp->sub.rects[0]->w * sp->sub.rects[0]->h;
//每个像素数据包含rgba四个字节
uint8_t buffered_img[len*4];
for(int i = 0; i < len; i++) {
//因为是bitmap类型,所以像素数据在sp->sub.rects[0]中分两块存储,data[1]中保存的是色表(palette),data[0]中保存的色表索引值
//从data[0]中拿到色表索引
int coloridx = sp->sub.rects[0]->data[0][i] * 4;
//到data[1]中拿到对应的颜色值
buffered_img[i*4] = sp->sub.rects[0]->data[1][coloridx];//R
buffered_img[i*4 + 1] = sp->sub.rects[0]->data[1][coloridx + 1];//G
buffered_img[i*4 + 2] = sp->sub.rects[0]->data[1][coloridx + 2];//B
buffered_img[i*4 + 3] = sp->sub.rects[0]->data[1][coloridx + 3];//A
}
//自定义一个新的消息类型FFP_MSG_BITMAP_SUBTITLE,用于传递像素数据
ffp_notify_msg4(ffp, FFP_MSG_BITMAP_SUBTITLE, sp->sub.rects[0]->w, sp->sub.rects[0]->h, buffered_img, len*4);
}
}
}
}
sp->uploaded = 1;
}
}
}
}
2.jni层的修改,在message_loop_n方法中,添加
case FFP_MSG_BITMAP_SUBTITLE:
if (msg.obj) {
int _width = msg.arg1;
int _height = msg.arg2;
uint8_t* bitmap = (uint8_t*) msg.obj;
jclass bitmapConfig = (*env)->FindClass(env, "android/graphics/Bitmap$Config");
jfieldID rgba8888FieldID = (*env)->GetStaticFieldID(env, bitmapConfig, "ARGB_8888", "Landroid/graphics/Bitmap$Config;");
jobject rgba8888Obj = (*env)->GetStaticObjectField(env, bitmapConfig, rgba8888FieldID);
jclass bitmapClass = (*env)->FindClass(env, "android/graphics/Bitmap");
jmethodID createBitmapMethodID = (*env)->GetStaticMethodID(env, bitmapClass,"createBitmap", "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
jobject bitmapObj = (*env)->CallStaticObjectMethod(env, bitmapClass, createBitmapMethodID, _width, _height, rgba8888Obj);
jintArray pixels = (*env)->NewIntArray(env, _width * _height);
for (int i = 0; i < _width * _height; i++)
{
unsigned char red = bitmap[i*4];
unsigned char green = bitmap[i*4 + 1];
unsigned char blue = bitmap[i*4 + 2];
unsigned char alpha = bitmap[i*4 + 3];
int currentPixel = (alpha << 24) | (red << 16) | (green << 8) | (blue);
(*env)->SetIntArrayRegion(env, pixels, i, 1, ¤tPixel);
}
jmethodID setPixelsMid = (*env)->GetMethodID(env, bitmapClass, "setPixels", "([IIIIIII)V");
(*env)->CallVoidMethod(env, bitmapObj, setPixelsMid, pixels, 0, _width, 0, 0, _width, _height);
post_event2(env, weak_thiz, MEDIA_BITMAP_SUBTITLE, msg.arg1, msg.arg2, bitmapObj);//自定义MEDIA_BITMAP_SUBTITLE消息,用于传递Bitmap对象
J4A_DeleteLocalRef__p(env, &bitmapObj);
} else {
post_event2(env, weak_thiz, MEDIA_BITMAP_SUBTITLE, 0, 0, NULL);
}
break;
3.java层的修改,在IjkMediaPlayer.java的EventHandler中,添加
case MEDIA_BITMAP_SUBTITLE:
if (msg.obj == null) {
player.notifyOnTimedText(null);
} else {
int bitmapWidth = msg.arg1;
int bitmapHeight = msg.arg2;
//拿到jni层传递过来的Bitmap对象
Bitmap bitmap = (Bitmap) msg.obj;
// Build the cue.
int planeWidth = player.mVideoWidth;
int planeHeight = player.mVideoHeight;
int bitmapX = (planeWidth - bitmapWidth) / 2;//默认在水平居中,靠近画面底部的位置显示字幕,所以可以简单计算出一个坐标值
int bitmapY = planeHeight - bitmapHeight - 50;
//创建字幕对象,稍后讲解
IjkSubtitle subtitle = new IjkSubtitle(
bitmap,
(float) bitmapX / planeWidth,
IjkSubtitle.ANCHOR_TYPE_START,
(float) bitmapY / planeHeight,
IjkSubtitle.ANCHOR_TYPE_START,
(float) bitmapWidth / planeWidth,
(float) bitmapHeight / planeHeight);
//通知到IjkVideoView中,进行显示
player.notifyOnTimedText(subtitle);
}
return;
在上面用到了一个IjkSubtitle类,这个类移植自ExoPlayer的com/google/android/exoplayer2/text/Cue.java。在ExoPlayer中有一套完整的图片与文字字幕渲染、样式修改的工具类,所以可以很方便的移植过来使用,包含以下几个类
com/google/android/exoplayer2/text/CaptionStyleCompat.java
com/google/android/exoplayer2/ui/SubtitleView.java
com/google/android/exoplayer2/ui/SubtitlePainter.java
由此,即完成了图片字幕的显示