Android平台RTSP|RTMP播放器高效率如何回调YUV或RGB数据?

​技术背景

我们在做Android平台RTSP、RTMP播放器的时候,经常遇到这样的技术诉求,开发者希望拿到播放器解码后的YUV或RGB数据,投递给视觉算法,做AI分析,本文以ffmpeg和大牛直播SDK的SmartPlayer为例,介绍下相关的技术实现。

FFmpeg

FFmpeg 是一个开源的跨平台多媒体处理工具库,广泛应用于音视频处理领域。

格式转换

  • 可以在众多不同的音频和视频格式之间进行转换。例如,将 MP4 格式转换为 AVI 格式,或者将 WAV 音频文件转换为 MP3 格式。
  • 支持几乎所有常见的音视频编码格式,如 H.264、H.265、AAC、MP3 等。

编码与解码

  • 能够对各种音视频编码格式进行解码,将压缩的音视频数据还原为原始的图像和音频信号。
  • 同时也可以进行编码操作,将原始的音视频数据压缩成特定的编码格式,以减小文件大小或满足特定的播放需求。
  1. 视频处理

    • 可以进行视频剪辑、拼接、裁剪等操作。比如,从一个长视频中截取特定的片段,或者将多个视频片段拼接成一个新的视频。
    • 支持视频的旋转、缩放、滤镜添加等特效处理。例如,将视频进行 90 度旋转,或者对视频应用模糊、锐化等滤镜效果。

音频处理

  • 可以进行音频的混音、提取、音量调整等操作。例如,将多个音频文件混合在一起,或者从视频中提取音频轨道。
  • 支持音频的均衡器调整、降噪等处理,以改善音频质量。

流媒体处理

  • 能够处理实时流媒体,如 RTMP、RTSP、HLS 等协议。可以进行流媒体的录制、转码、分发等操作。
  • 对于直播场景,FFmpeg 可以作为推流或拉流的工具,实现视频直播的采集、编码和传输。

集成 FFmpeg

  • 将 FFmpeg 库集成到 Android 项目中,可以通过使用 Android NDK 来编译和链接 FFmpeg 库。
  • 配置项目的build.gradle文件,添加 NDK 相关的配置,并创建一个 JNI 层的接口来调用 FFmpeg 的功能。

利用 FFmpeg 解码视频并获取 YUV 数据

  • 在 JNI 层的代码中,使用 FFmpeg 的解码功能来解码 RTSP/RTMP 视频流。FFmpeg 提供了丰富的 API 来处理各种多媒体格式。

  • 在解码过程中,可以获取解码后的视频帧,并将其转换为 YUV 格式的数据。然后通过 JNI 回调将 YUV 数据传递到 Java 层。

  • 以下是一个简单的 JNI 方法示例,用于解码视频并回调 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;

    ini 复制代码
     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; }

    scss 复制代码
    		 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); }

    scss 复制代码
     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

SmartPlayer是大牛直播SDK旗下全自研内核,行业内一致认可的跨平台RTSP、RTMP直播播放器SDK,功能齐全、高稳定、超低延迟,超低资源占用,适用于安防、教育、单兵指挥等行业。

功能设计如下:

  • [支持播放协议]高稳定、超低延迟、业内首屈一指的RTSP直播播放器SDK;
  • [多实例播放]支持多实例播放;
  • [事件回调]支持网络状态、buffer状态等回调;
  • [视频格式]支持H.265、H.264,此外,还支持RTSP MJPEG播放;
  • [音频格式]支持AAC/PCMA/PCMU;
  • [H.264/H.265软解码]支持H.264/H.265软解;
  • [H.264硬解码]Windows/Android/iOS支持特定机型H.264硬解;
  • [H.265硬解]Windows/Android/iOS支持特定机型H.265硬解;
  • [H.264/H.265硬解码]Android支持设置Surface模式硬解和普通模式硬解码;
  • [RTSP模式设置]支持RTSP TCP/UDP模式设置;
  • [RTSP TCP/UDP自动切换]支持RTSP TCP、UDP模式自动切换;
  • [RTSP超时设置]支持RTSP超时时间设置,单位:秒;
  • [RTSP 401认证处理]支持上报RTSP 401事件,如URL携带鉴权信息,会自动处理;
  • [缓冲时间设置]支持buffer time设置;
  • [首屏秒开]支持首屏秒开模式;
  • [复杂网络处理]支持断网重连等各种网络环境自动适配;
  • [快速切换URL]支持播放过程中,快速切换其他URL,内容切换更快;
  • [音视频多种render机制]Android平台,视频:surfaceview/OpenGL ES,音频:AudioTrack/OpenSL ES;
  • [实时静音]支持播放过程中,实时静音/取消静音;
  • [实时音量调节]支持播放过程中实时调节音量;
  • [实时快照]支持播放过程中截取当前播放画面;
  • [只播关键帧]Windows平台支持实时设置是否只播放关键帧;
  • [渲染角度]支持0°,90°,180°和270°四个视频画面渲染角度设置;
  • [渲染镜像]支持水平反转、垂直反转模式设置;
  • [等比例缩放]支持图像等比例缩放绘制(Android设置surface模式硬解模式不支持);
  • [实时下载速度更新]支持当前下载速度实时回调(支持设置回调时间间隔);
  • [解码前视频数据回调]支持H.264/H.265数据回调;
  • [解码后视频数据回调]支持解码后YUV/RGB数据回调;
  • [解码前音频数据回调]支持AAC/PCMA/PCMU数据回调;
  • [音视频自适应]支持播放过程中,音视频信息改变后自适应;
  • [扩展录像功能]完美支持和录像SDK组合使用。

播放之前,设置YUV数据回调:

ini 复制代码
/*
 * 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数据回调上层实现:

ini 复制代码
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,各有利弊,感兴趣的开发者,可以单独跟我探讨。

相关推荐
启明智显3 天前
视频直播5G CPE解决方案:ZX7981PG/ZX7981PMWIFI6网络覆盖
5g·直播·wifi6·5g cpe·无线路由器
音视频牛哥6 天前
Android平台如何拉取RTSP|RTMP流并转发至轻量级RTSP服务?
音视频开发·视频编码·直播
声知视界6 天前
音视频基础能力之 iOS 视频篇(一):视频采集
音视频开发
fareast_mzh7 天前
Setting Up a Simple Live Streaming Server on Debian 11
运维·debian·直播
关键帧Keyframe9 天前
音视频面试题集锦第 15 期 | 编辑 SDK 架构 | 直播回声 | 播放器架构
音视频开发·视频编码·客户端
伊织code11 天前
[2024最新] macOS 发起 Bilibili 直播(不使用 OBS)
macos·mac·web·直播·b站·bilibili
音视频开发技术13 天前
cannot locate symbol _ZTVNSt6__ndk119basic_ostringstreamIcNS_
android·直播
关键帧Keyframe14 天前
iOS 不用 libyuv 也能高效实现 RGB/YUV 数据转换丨音视频工业实战
音视频开发·视频编码·客户端
关键帧Keyframe16 天前
音视频面试题集锦第 7 期
音视频开发·视频编码·客户端
关键帧Keyframe16 天前
音视频面试题集锦第 8 期
ios·音视频开发·客户端