我们在做Android平台RTSP、RTMP播放器的时候,经常遇到这样的技术诉求,开发者希望拿到播放器解码后的YUV或RGB数据,投递给视觉算法,做AI分析,本文以ffmpeg和大牛直播SDK的SmartPlayer为例,介绍下相关的技术实现。
FFmpeg 是一个开源的跨平台多媒体处理工具库,广泛应用于音视频处理领域。
格式转换:
编码与解码:
音频处理:
流媒体处理:
集成 FFmpeg
build.gradle
文件,添加 NDK 相关的配置,并创建一个 JNI 层的接口来调用 FFmpeg 的功能。利用 FFmpeg 解码视频并获取 YUV 数据
#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <android/log.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#define LOG_TAG "FFmpegNative"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
AVFormatContext *pFormatCtx = NULL;
AVCodecContext *pCodecCtx = NULL;
AVCodec *pCodec = NULL;
AVFrame *pFrame = NULL;
AVPacket packet;
struct SwsContext *img_convert_ctx = NULL;
jobject yuvCallbackObj;
jmethodID yuvCallbackMethod;
void Java_com_example_myapp_MyNativeLib_init(JNIEnv *env, jobject obj, jstring url, jobject callbackObj, jmethodID callbackMethod) {
const char *input_filename = (*env)->GetStringUTFChars(env, url, NULL);
yuvCallbackObj = (*env)->NewGlobalRef(env, callbackObj);
yuvCallbackMethod = callbackMethod;
av_register_all();
avformat_network_init();
if (avformat_open_input(&pFormatCtx, input_filename, NULL, NULL)!= 0) {
LOGE("Could not open input file.");
return;
}
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
LOGE("Could not find stream information.");
return;
}
int videoStream = -1;
for (int i = 0; i < pFormatCtx->nb_streams; i++) {
if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStream = i;
break;
}
}
if (videoStream == -1) {
LOGE("Could not find video stream.");
return;
}
pCodecCtx = avcodec_alloc_context3(NULL);
if (!pCodecCtx) {
LOGE("Could not allocate video codec context.");
return;
}
avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[videoStream]->codecpar);
pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
if (!pCodec) {
LOGE("Could not find codec.");
return;
}
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
LOGE("Could not open codec.");
return;
}
pFrame = av_frame_alloc();
if (!pFrame) {
LOGE("Could not allocate video frame.");
return;
}
}
void Java_com_example_myapp_MyNativeLib_decodeAndCallback(JNIEnv *env, jobject obj) {
while (av_read_frame(pFormatCtx, &packet) >= 0) {
if (packet.stream_index == videoStream) {
int ret = avcodec_send_packet(pCodecCtx, &packet);
if (ret < 0) {
LOGE("Error sending a packet for decoding.");
av_packet_unref(&packet);
continue;
}
while (ret >= 0) {
ret = avcodec_receive_frame(pCodecCtx, pFrame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
LOGE("Error during decoding.");
break;
}
// 将解码后的帧转换为 YUV 格式
if (img_convert_ctx == NULL) {
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height,
pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height,
AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
if (!img_convert_ctx) {
LOGE("Could not initialize the conversion context.");
return;
}
}
AVFrame *yuvFrame = av_frame_alloc();
if (!yuvFrame) {
LOGE("Could not allocate YUV frame.");
return;
}
yuvFrame->format = AV_PIX_FMT_YUV420P;
yuvFrame->width = pCodecCtx->width;
yuvFrame->height = pCodecCtx->height;
int bufferSize = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1);
uint8_t *buffer = (uint8_t *)av_malloc(bufferSize);
av_image_fill_arrays(yuvFrame->data, yuvFrame->linesize, buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1);
sws_scale(img_convert_ctx, (const uint8_t *const *)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, yuvFrame->data, yuvFrame->linesize);
// 回调 YUV 数据到 Java 层
(*env)->CallVoidMethod(env, yuvCallbackObj, yuvCallbackMethod, buffer, bufferSize);
av_free(buffer);
av_frame_free(&yuvFrame);
}
}
av_packet_unref(&packet);
}
}
void Java_com_example_myapp_MyNativeLib_release(JNIEnv *env, jobject obj) {
if (pFormatCtx) {
avformat_close_input(&pFormatCtx);
}
if (pCodecCtx) {
avcodec_close(pCodecCtx);
avcodec_free_context(&pCodecCtx);
}
if (pFrame) {
av_frame_free(&pFrame);
}
if (img_convert_ctx) {
sws_freeContext(img_convert_ctx);
}
(*env)->DeleteGlobalRef(env, yuvCallbackObj);
}
SmartPlayer是大牛直播SDK旗下全自研内核,行业内一致认可的跨平台RTSP、RTMP直播播放器SDK,功能齐全、高稳定、超低延迟,超低资源占用,适用于安防、教育、单兵指挥等行业。
功能设计如下:
播放之前,设置YUV数据回调:
/*
* SmartPlayer.java
* Copyright © 2014~2024 daniusdk.com All rights reserved.
*/
private boolean StartPlay()
{
if(isPlaying)
return false;
if(!isPulling)
{
if (!OpenPullHandle())
return false;
}
// 如果第二个参数设置为null,则播放纯音频
libPlayer.SmartPlayerSetSurface(player_handle_, sSurfaceView);
//libPlayer.SmartPlayerSetSurface(player_handle_, null);
libPlayer.SmartPlayerSetRenderScaleMode(player_handle_, 1);
if(video_opt_ == 3)
{
libPlayer.SmartPlayerSetExternalRender(player_handle_, new I420ExternalRender(publisher_array_));
}
//libPlayer.SmartPlayerSetExternalAudioOutput(player_handle_, new PlayerExternalPCMOutput(stream_publisher_));
libPlayer.SmartPlayerSetFastStartup(player_handle_, isFastStartup ? 1 : 0);
libPlayer.SmartPlayerSetAudioOutputType(player_handle_, 1);
if (isMute) {
libPlayer.SmartPlayerSetMute(player_handle_, isMute ? 1 : 0);
}
if (isHardwareDecoder)
{
int isSupportH264HwDecoder = libPlayer.SetSmartPlayerVideoHWDecoder(player_handle_, 1);
int isSupportHevcHwDecoder = libPlayer.SetSmartPlayerVideoHevcHWDecoder(player_handle_, 1);
Log.i(TAG, "isSupportH264HwDecoder: " + isSupportH264HwDecoder + ", isSupportHevcHwDecoder: " + isSupportHevcHwDecoder);
}
libPlayer.SmartPlayerSetLowLatencyMode(player_handle_, isLowLatency ? 1 : 0);
libPlayer.SmartPlayerSetRotation(player_handle_, rotate_degrees);
int iPlaybackRet = libPlayer.SmartPlayerStartPlay(player_handle_);
if (iPlaybackRet != 0 && !isPulling) {
Log.e(TAG, "StartPlay failed!");
releasePlayerHandle();
return false;
}
isPlaying = true;
return true;
}
YUV数据回调上层实现:
private static class I420ExternalRender implements NTExternalRender {
// public static final int NT_FRAME_FORMAT_RGBA = 1;
// public static final int NT_FRAME_FORMAT_ABGR = 2;
// public static final int NT_FRAME_FORMAT_I420 = 3;
private WeakReference<LibPublisherWrapper> publisher_;
private ArrayList<WeakReference<LibPublisherWrapper> > publisher_list_;
private int width_;
private int height_;
private int y_row_bytes_;
private int u_row_bytes_;
private int v_row_bytes_;
private ByteBuffer y_buffer_;
private ByteBuffer u_buffer_;
private ByteBuffer v_buffer_;
public I420ExternalRender(LibPublisherWrapper publisher) {
if (publisher != null)
publisher_ = new WeakReference<>(publisher);
}
public I420ExternalRender(LibPublisherWrapper[] publisher_list) {
if (publisher_list != null && publisher_list.length > 0) {
for (LibPublisherWrapper i : publisher_list) {
if (i != null) {
if (null == publisher_list_)
publisher_list_ = new ArrayList<>();
publisher_list_.add(new WeakReference<>(i));
}
}
}
}
private final List<LibPublisherWrapper> get_publisher_list() {
if (null == publisher_list_ || publisher_list_.isEmpty())
return null;
ArrayList<LibPublisherWrapper> list = new ArrayList<>(publisher_list_.size());
for (WeakReference<LibPublisherWrapper> i : publisher_list_) {
if (i != null) {
LibPublisherWrapper o = i.get();
if (o != null && !o.empty())
list.add(o);
}
}
return list;
}
@Override
public int getNTFrameFormat() {
Log.i(TAG, "I420ExternalRender::getNTFrameFormat return "
+ NT_FRAME_FORMAT_I420);
return NT_FRAME_FORMAT_I420;
}
private static int align(int d, int a) { return (d + (a - 1)) & ~(a - 1); }
@Override
public void onNTFrameSizeChanged(int width, int height) {
width_ = width;
height_ = height;
y_row_bytes_ = width;
u_row_bytes_ = (width+1)/2;
v_row_bytes_ = (width+1)/2;
y_buffer_ = ByteBuffer.allocateDirect(y_row_bytes_*height_);
u_buffer_ = ByteBuffer.allocateDirect(u_row_bytes_*((height_ + 1) / 2));
v_buffer_ = ByteBuffer.allocateDirect(v_row_bytes_*((height_ + 1) / 2));
}
@Override
public ByteBuffer getNTPlaneByteBuffer(int index) {
switch (index) {
case 0:
return y_buffer_;
case 1:
return u_buffer_;
case 2:
return v_buffer_;
default:
Log.e(TAG, "I420ExternalRender::getNTPlaneByteBuffer index error:" + index);
return null;
}
}
@Override
public int getNTPlanePerRowBytes(int index) {
switch (index) {
case 0:
return y_row_bytes_;
case 1:
return u_row_bytes_;
case 2:
return v_row_bytes_;
default:
Log.e(TAG, "I420ExternalRender::getNTPlanePerRowBytes index error:" + index);
return 0;
}
}
public void onNTRenderFrame(int width, int height, long timestamp)
{
if (null == y_buffer_ || null == u_buffer_ || null == v_buffer_)
return;
Log.i(TAG, "I420ExternalRender::onNTRenderFrame " + width + "*" + height + ", t:" + timestamp);
y_buffer_.rewind();
u_buffer_.rewind();
v_buffer_.rewind();
List<LibPublisherWrapper> publisher_list = get_publisher_list();
if (null == publisher_list || publisher_list.isEmpty())
return;
for (LibPublisherWrapper i : publisher_list) {
i.PostLayerImageI420ByteBuffer(0, 0, 0,
y_buffer_, 0, y_row_bytes_,
u_buffer_, 0, u_row_bytes_,
v_buffer_, 0, v_row_bytes_,
width_, height_, 0, 0,
0,0, 0,0);
}
}
}
Android平台RTSP、RTMP播放器回调yuv数据,意义非常重大,既保证了低延迟传输解码,又可以通过回调解码后数据,高效率的投递给AI算法,实现视觉处理。ffmpeg实现还是SmartPlayer,各有利弊,感兴趣的开发者,可以单独跟我探讨。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。