之前业务需要,在android在播放udp传输的h265视频流,播放效果还行,但延迟不太满意,半秒以上,后面听说用c来解码快一点,就试了一下,结果没卵区别。不过做都做了,把代码写下来记录一下。
首页是界面代码,实现了两个jni接口用于播放和停止播放,同时给surfaceview添加回调:
MainActivity:
class MainActivity : AppCompatActivity(),SurfaceHolder.Callback {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.surfaceview.holder.addCallback(this)
}
override fun surfaceCreated(holder: SurfaceHolder) {
Thread {
startNativePlayback(holder.surface, 50002)
}.start()
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
}
private external fun startNativePlayback(surface: Surface, port: Int)
private external fun stopNativePlayback()
companion object {
init {
System.loadLibrary("mediacodecnative")
}
}
override fun onDestroy() {
super.onDestroy()
stopNativePlayback()
}
}
界面布局,就一个surfaceview
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<SurfaceView
android:id="@+id/surfaceview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
C++代码,首先是创建udp监听50002端口,接收到数据后发到mediacodec进行解码播放
native-lib.cpp:
#include <jni.h>
#include <string>
#include <jni.h>
#include <android/native_window_jni.h>
#include <media/NdkMediaCodec.h>
#include <media/NdkMediaFormat.h>
#include <pthread.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <android/log.h>
#include <vector>
#define LOG_TAG "###HEVCDecoder"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
struct DecoderContext {
AMediaCodec *codec = nullptr;
ANativeWindow *window = nullptr;
int sockfd = -1;
bool running = false;
pthread_t thread;
int64_t timestampUs = 0;
};
static DecoderContext *gCtx = nullptr;
static void *receiverThread(void *arg);
static void processNalUnit(DecoderContext *ctx, uint8_t *data, size_t size);
static void configureDecoder(DecoderContext *ctx);
extern "C" JNIEXPORT void JNICALL Java_com_test_mediacodecnative_MainActivity_stopNativePlayback(JNIEnv *, jobject);
extern "C" JNIEXPORT void JNICALL
Java_com_test_mediacodecnative_MainActivity_startNativePlayback(
JNIEnv *env,
jobject thiz,
jobject surface,
jint port) {
LOGD("startNativePlayback");
if (gCtx) {
Java_com_test_mediacodecnative_MainActivity_stopNativePlayback(env, thiz);
}
auto *ctx = new DecoderContext();
gCtx = ctx;
ctx->window = ANativeWindow_fromSurface(env, surface);
configureDecoder(ctx);
// Create UDP socket
ctx->sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (ctx->sockfd < 0) {
LOGE("Socket creation failed");
delete ctx;
return;
}
sockaddr_in server_addr{};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
server_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(ctx->sockfd, (sockaddr *) &server_addr, sizeof(server_addr))) {
LOGE("Bind failed: %s", strerror(errno));
close(ctx->sockfd);
delete ctx;
return;
}
// Start receiver thread
ctx->running = true;
if (pthread_create(&ctx->thread, nullptr, receiverThread, ctx)) {
LOGE("Thread creation failed");
close(ctx->sockfd);
delete ctx;
}
}
extern "C" JNIEXPORT void JNICALL
Java_com_test_mediacodecnative_MainActivity_stopNativePlayback(
JNIEnv *env,
jobject thiz) {
if (!gCtx) {
LOGD("Playback already stopped");
return;
}
// 停止接收线程
gCtx->running = false;
pthread_join(gCtx->thread, nullptr);
LOGD("Receiver thread stopped");
// 关闭网络套接字
if (gCtx->sockfd != -1) {
close(gCtx->sockfd);
gCtx->sockfd = -1;
LOGD("Socket closed");
}
// 释放解码器资源
if (gCtx->codec) {
AMediaCodec_stop(gCtx->codec);
AMediaCodec_delete(gCtx->codec);
gCtx->codec = nullptr;
LOGD("MediaCodec released");
}
// 释放NativeWindow
if (gCtx->window) {
ANativeWindow_release(gCtx->window);
gCtx->window = nullptr;
LOGD("Surface released");
}
// 删除上下文对象
delete gCtx;
gCtx = nullptr;
LOGD("Context cleanup completed");
}
static void *receiverThread(void *arg) {
LOGD("receiverThread");
auto *ctx = static_cast<DecoderContext *>(arg);
constexpr int MAX_UDP_SIZE = 1024*10;
uint8_t buffer[MAX_UDP_SIZE];
int64_t frameInterval = 1000000 / 30; // 30fps
while (ctx->running) {
sockaddr_in client_addr{};
socklen_t addr_len = sizeof(client_addr);
ssize_t count = recvfrom(
ctx->sockfd, buffer, MAX_UDP_SIZE, 0,
(sockaddr *) &client_addr, &addr_len
);
LOGD("receive date count = %d",count);
if (count > 0) {
processNalUnit(ctx, buffer, count);
ctx->timestampUs += frameInterval;
} else if (count < 0) {
LOGE("recvfrom error: %s", strerror(errno));
}
}
close(ctx->sockfd);
ctx->sockfd = -1;
return nullptr;
}
static std::vector<uint8_t> s_buffer;
static void processNalUnit(DecoderContext *ctx, uint8_t *data, size_t size) {
if (!ctx->codec) {
LOGE("codec unknown");
return;
}
uint8_t* data1;
size_t size1;
if (size >= 4 && memcmp(data, "\x00\x00\x00\x01", 4) == 0) {//偷懒只判断00000001开头的NAL单元,不是严格判断NAL单元开头
size1= s_buffer.size();
data1 = new uint8_t[size1];
memcpy(data1, s_buffer.data(), size1);
s_buffer.clear();
s_buffer.insert(s_buffer.end(), data, data + size);
}else{
s_buffer.insert(s_buffer.end(), data, data + size);
return;
}
ssize_t idx = AMediaCodec_dequeueInputBuffer(ctx->codec, 1000);
if (idx >= 0) {
size_t bufSize;
uint8_t *buf = AMediaCodec_getInputBuffer(ctx->codec, idx, &bufSize);
if (buf && bufSize >= size1) {
memcpy(buf, data1, size1);
AMediaCodec_queueInputBuffer(
ctx->codec, idx, 0, size1, ctx->timestampUs, 0);
}
}
AMediaCodecBufferInfo info;
ssize_t outIdx = AMediaCodec_dequeueOutputBuffer(ctx->codec, &info, 1000);
if (outIdx >= 0) {
AMediaCodec_releaseOutputBuffer(ctx->codec, outIdx, true);
}
}
static void configureDecoder(DecoderContext *ctx) {
int width = 1280, height = 720;
if (ctx->codec) {
AMediaCodec_stop(ctx->codec);
AMediaCodec_delete(ctx->codec);
}
ctx->codec = AMediaCodec_createDecoderByType("video/hevc");
AMediaFormat *format = AMediaFormat_new();
AMediaFormat_setString(format, AMEDIAFORMAT_KEY_MIME, "video/hevc");
AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_WIDTH, width);
AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_HEIGHT, height);
AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_FRAME_RATE, 30);
if (AMediaCodec_configure(ctx->codec, format, ctx->window, nullptr, 0) != AMEDIA_OK) {
LOGE("Decoder configuration failed");
}
AMediaCodec_start(ctx->codec);
AMediaFormat_delete(format);
}
CMake配置
CMakeLists.txt:
add_library(${CMAKE_PROJECT_NAME} SHARED
native-lib.cpp)
target_link_libraries(${CMAKE_PROJECT_NAME}
android
mediandk
log)
别忘了添加网络权限
<uses-permission android:name="android.permission.INTERNET" />
测试视频可以直接用ffmpeg推流电脑摄像头:
ffmpeg -f dshow -i video="USB Video Device" -tune zerolatency -c:v libx265 -pix_fmt yuv420p -preset ultrafast -s 1280x720 -f hevc udp://xxx.xxx.xxx.xxx:50002