diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..1b1780b --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,72 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'com.xypower.dblstreams' + compileSdk 33 + + defaultConfig { + applicationId "com.xypower.dblstreams" + minSdk 24 + targetSdk 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + externalNativeBuild { + cmake { + // cppFlags '-std=c++17 -frtti -fexceptions -Wno-error=format-security' + cppFlags '-std=c++17 -fexceptions -Wno-error=format-security -fopenmp' + // cppFlags '-std=c++17 -Wno-error=format-security' + // arguments "-DANDROID_STL=c++_shared" + arguments "-DHDRPLUS_ROOT=" + hdrplusroot + abiFilters 'arm64-v8a' + // setAbiFilters(['arm64-v8a']) + } + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + debuggable true + jniDebuggable true + } + } + + sourceSets { + main { + jniLibs.srcDirs = ['libs'] + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + externalNativeBuild { + cmake { + path file('src/main/cpp/CMakeLists.txt') + version '3.22.1' + } + } + buildFeatures { + viewBinding true + } +} + +dependencies { + + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.8.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + + // https://mvnrepository.com/artifact/com.arthenica/ffmpeg-kit-full + // implementation files('libs/ffmpeg-kit-full-6.0-2.LTS.aar') +} \ No newline at end of file diff --git a/app/libs/ffmpeg-kit-full-6.0-2.LTS.aar b/app/libs/ffmpeg-kit-full-6.0-2.LTS.aar new file mode 100644 index 0000000..d40eacd Binary files /dev/null and b/app/libs/ffmpeg-kit-full-6.0-2.LTS.aar differ diff --git a/app/libs/ffmpeg-kit-full-6.0-2.aar b/app/libs/ffmpeg-kit-full-6.0-2.aar new file mode 100644 index 0000000..be74477 Binary files /dev/null and b/app/libs/ffmpeg-kit-full-6.0-2.aar differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/xypower/dblstreams/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/xypower/dblstreams/ExampleInstrumentedTest.java new file mode 100644 index 0000000..381d779 --- /dev/null +++ b/app/src/androidTest/java/com/xypower/dblstreams/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.xypower.dblstreams; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.xypower.dblstreams", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..59203be --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000..5247280 --- /dev/null +++ b/app/src/main/cpp/CMakeLists.txt @@ -0,0 +1,73 @@ +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html. +# For more examples on how to use CMake, see https://github.com/android/ndk-samples. + +# Sets the minimum CMake version required for this project. +cmake_minimum_required(VERSION 3.22.1) + +# Declares the project name. The project name can be accessed via ${ PROJECT_NAME}, +# Since this is the top level CMakeLists.txt, the project name is also accessible +# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level +# build script scope). +project("dblstreams") + +add_definitions(-DUSING_FFMPEG) + +# Find required packages +# find_package(camera2ndk REQUIRED) +# find_package(mediandk REQUIRED) + +# Find FFmpeg +#find_path(FFMPEG_INCLUDE_DIR libavformat/avformat.h) +#find_library(AVCODEC_LIBRARY avcodec) +#find_library(AVFORMAT_LIBRARY avformat) +#find_library(AVUTIL_LIBRARY avutil) + +# OpenMP +find_package(OpenMP REQUIRED) + +include_directories(D:/Workspace/deps/hdrplus_libs/${ANDROID_ABI}/include) +link_directories(D:/Workspace/deps/hdrplus_libs/${ANDROID_ABI}/lib) + +include_directories( + ${CMAKE_SOURCE_DIR}/include + ${FFMPEG_INCLUDE_DIR} +) + +# Creates and names a library, sets it as either STATIC +# or SHARED, and provides the relative paths to its source code. +# You can define multiple libraries, and CMake builds them for you. +# Gradle automatically packages shared libraries with your APK. +# +# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define +# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME} +# is preferred for the same purpose. +# +# In order to load a library into your app from Java/Kotlin, you must call +# System.loadLibrary() and pass the name of the library defined here; +# for GameActivity/NativeActivity derived applications, the same library name must be +# used in the AndroidManifest.xml file. +add_library(${CMAKE_PROJECT_NAME} SHARED + # List C/C++ source files with relative paths to this CMakeLists.txt. + native-lib.cpp + camera_manager.cpp + encoder_manager.cpp + rtsp_streamer.cpp + Utils.cpp + + ) + +# Specifies libraries CMake should link to your target library. You +# can link libraries from various origins, such as libraries defined in this +# build script, prebuilt third-party libraries, or Android system libraries. +target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC -fopenmp -static-openmp + # List libraries link to the target library + android + log + camera2ndk + mediandk + z m + -pthread + avcodec avfilter avformat avutil swresample swscale x264 postproc + OpenMP::OpenMP_CXX + ) \ No newline at end of file diff --git a/app/src/main/cpp/Utils.cpp b/app/src/main/cpp/Utils.cpp new file mode 100644 index 0000000..7cdbb19 --- /dev/null +++ b/app/src/main/cpp/Utils.cpp @@ -0,0 +1,42 @@ +#include "utils.h" +#include +#include + +namespace utils { + + void YUV420ToNV21(const uint8_t* yuv420, uint8_t* nv21, int width, int height) { + int frameSize = width * height; + int uSize = frameSize / 4; + int vSize = frameSize / 4; + + // Y + memcpy(nv21, yuv420, frameSize); + + // VU (interleaved) + for (int i = 0; i < uSize; i++) { + nv21[frameSize + i * 2] = yuv420[frameSize + uSize + i]; // V + nv21[frameSize + i * 2 + 1] = yuv420[frameSize + i]; // U + } + } + + void YUV420ToNV12(const uint8_t* yuv420, uint8_t* nv12, int width, int height) { + int frameSize = width * height; + int uSize = frameSize / 4; + int vSize = frameSize / 4; + + // Y + memcpy(nv12, yuv420, frameSize); + + // UV (interleaved) + for (int i = 0; i < uSize; i++) { + nv12[frameSize + i * 2] = yuv420[frameSize + i]; // U + nv12[frameSize + i * 2 + 1] = yuv420[frameSize + uSize + i]; // V + } + } + + int64_t getCurrentTimeMicro() { + return std::chrono::duration_cast( + std::chrono::high_resolution_clock::now().time_since_epoch()).count(); + } + +} // namespace utils \ No newline at end of file diff --git a/app/src/main/cpp/Utils.h b/app/src/main/cpp/Utils.h new file mode 100644 index 0000000..ebe3317 --- /dev/null +++ b/app/src/main/cpp/Utils.h @@ -0,0 +1,21 @@ +#ifndef UTILS_H +#define UTILS_H + +#include +#include + +// YUV conversion utilities +namespace utils { + +// Convert YUV420 to NV21 + void YUV420ToNV21(const uint8_t* yuv420, uint8_t* nv21, int width, int height); + +// Convert YUV420 to NV12 + void YUV420ToNV12(const uint8_t* yuv420, uint8_t* nv12, int width, int height); + +// Get current timestamp in microseconds + int64_t getCurrentTimeMicro(); + +} // namespace utils + +#endif // UTILS_H \ No newline at end of file diff --git a/app/src/main/cpp/camera_manager.cpp b/app/src/main/cpp/camera_manager.cpp new file mode 100644 index 0000000..35178f3 --- /dev/null +++ b/app/src/main/cpp/camera_manager.cpp @@ -0,0 +1,243 @@ +#include "camera_manager.h" +#include +#include +#include +#include + +#define LOG_TAG "CameraManager" +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) + +CameraManager::CameraManager() {} + +CameraManager::~CameraManager() { + stopCapture(); + + if (mCameraDevice) { + ACameraDevice_close(mCameraDevice); + mCameraDevice = nullptr; + } + + if (mCameraManager) { + ACameraManager_delete(mCameraManager); + mCameraManager = nullptr; + } +} + +bool CameraManager::initialize() { + mCameraManager = ACameraManager_create(); + if (!mCameraManager) { + LOGE("Failed to create camera manager"); + return false; + } + return true; +} + +std::vector CameraManager::getAvailableCameras() { + std::vector cameraIds; + ACameraIdList* cameraIdList = nullptr; + + ACameraManager_getCameraIdList(mCameraManager, &cameraIdList); + if (cameraIdList) { + for (int i = 0; i < cameraIdList->numCameras; i++) { + cameraIds.push_back(cameraIdList->cameraIds[i]); + } + ACameraManager_deleteCameraIdList(cameraIdList); + } + + return cameraIds; +} + +bool CameraManager::openCamera(const char* cameraId) { + ACameraDevice_stateCallbacks deviceStateCallbacks = { + .context = this, + .onDisconnected = onDeviceDisconnected, + .onError = onDeviceError + }; + + camera_status_t status = ACameraManager_openCamera(mCameraManager, cameraId, &deviceStateCallbacks, &mCameraDevice); + if (status != ACAMERA_OK || !mCameraDevice) { + LOGE("Failed to open camera: %d", status); + return false; + } + + return true; +} + +bool CameraManager::startCapture(int width, int height, FrameCallback callback) { + mFrameCallback = callback; + + // Create ImageReader + media_status_t mediaStatus = AImageReader_new( + width, height, AIMAGE_FORMAT_YUV_420_888, 2, &mImageReader); + + if (mediaStatus != AMEDIA_OK || !mImageReader) { + LOGE("Failed to create image reader: %d", mediaStatus); + return false; + } + + // Set image reader callback + AImageReader_ImageListener listener = { + .context = this, + .onImageAvailable = imageCallback + }; + + AImageReader_setImageListener(mImageReader, &listener); + + // Create output target + ANativeWindow* nativeWindow; + AImageReader_getWindow(mImageReader, &nativeWindow); + ACameraOutputTarget_create(nativeWindow, &mOutputTarget); + + // Create capture request + ACameraDevice_createCaptureRequest(mCameraDevice, TEMPLATE_RECORD, &mCaptureRequest); + ACaptureRequest_addTarget(mCaptureRequest, mOutputTarget); + + // Configure session + ACaptureSessionOutput* sessionOutput; + ACameraOutputTarget* outputTarget; + ACaptureSessionOutputContainer* outputContainer; + + ACaptureSessionOutput_create(nativeWindow, &sessionOutput); + ACaptureSessionOutputContainer_create(&outputContainer); + ACaptureSessionOutputContainer_add(outputContainer, sessionOutput); + + ACameraCaptureSession_stateCallbacks sessionStateCallbacks = { + .context = this, + .onClosed = onSessionClosed, + .onReady = onSessionReady, + .onActive = onSessionActive + }; + + camera_status_t status = ACameraDevice_createCaptureSession( + mCameraDevice, outputContainer, &sessionStateCallbacks, &mCaptureSession); + + if (status != ACAMERA_OK) { + LOGE("Failed to create capture session: %d", status); + return false; + } + + // Start repeating request + status = ACameraCaptureSession_setRepeatingRequest( + mCaptureSession, nullptr, 1, &mCaptureRequest, nullptr); + + if (status != ACAMERA_OK) { + LOGE("Failed to start repeating request: %d", status); + return false; + } + + mRunning = true; + return true; +} + +void CameraManager::stopCapture() { + std::unique_lock lock(mMutex); + if (mRunning) { + mRunning = false; + lock.unlock(); + mCondVar.notify_all(); + + if (mCaptureSession) { + ACameraCaptureSession_stopRepeating(mCaptureSession); + ACameraCaptureSession_close(mCaptureSession); + mCaptureSession = nullptr; + } + + if (mCaptureRequest) { + ACaptureRequest_free(mCaptureRequest); + mCaptureRequest = nullptr; + } + + if (mOutputTarget) { + ACameraOutputTarget_free(mOutputTarget); + mOutputTarget = nullptr; + } + + if (mImageReader) { + AImageReader_delete(mImageReader); + mImageReader = nullptr; + } + } +} + +// Static callbacks +void CameraManager::onDeviceDisconnected(void* context, ACameraDevice* device) { + auto* manager = static_cast(context); + LOGI("Camera disconnected"); + manager->stopCapture(); +} + +void CameraManager::onDeviceError(void* context, ACameraDevice* device, int error) { + auto* manager = static_cast(context); + LOGE("Camera error: %d", error); + manager->stopCapture(); +} + +void CameraManager::onSessionClosed(void* context, ACameraCaptureSession* session) { + LOGI("Camera session closed"); +} + +void CameraManager::onSessionReady(void* context, ACameraCaptureSession* session) { + LOGI("Camera session ready"); +} + +void CameraManager::onSessionActive(void* context, ACameraCaptureSession* session) { + LOGI("Camera session active"); +} + +void CameraManager::imageCallback(void* context, AImageReader* reader) { + auto* manager = static_cast(context); + AImage* image = nullptr; + + media_status_t status = AImageReader_acquireLatestImage(reader, &image); + if (status != AMEDIA_OK || !image) { + return; + } + + // Get image data + int32_t format; + AImage_getFormat(image, &format); + + int32_t width, height; + AImage_getWidth(image, &width); + AImage_getHeight(image, &height); + + int64_t timestamp; + AImage_getTimestamp(image, ×tamp); + + uint8_t* data = nullptr; + int dataLength = 0; + + // For YUV420 format, we need to get each plane + uint8_t* yPixel = nullptr; + uint8_t* uPixel = nullptr; + uint8_t* vPixel = nullptr; + int yLen = 0, uLen = 0, vLen = 0; + int yStride = 0, uStride = 0, vStride = 0; + + AImage_getPlaneData(image, 0, &yPixel, &yLen); + AImage_getPlaneData(image, 1, &uPixel, &uLen); + AImage_getPlaneData(image, 2, &vPixel, &vLen); + AImage_getPlaneRowStride(image, 0, &yStride); + AImage_getPlaneRowStride(image, 1, &uStride); + AImage_getPlaneRowStride(image, 2, &vStride); + + // Assuming a continuous buffer (might need to copy to a contiguous buffer) + int totalSize = yLen + uLen + vLen; + uint8_t* buffer = new uint8_t[totalSize]; + + // Copy Y plane + memcpy(buffer, yPixel, yLen); + // Copy U plane + memcpy(buffer + yLen, uPixel, uLen); + // Copy V plane + memcpy(buffer + yLen + uLen, vPixel, vLen); + + // Process frame in callback + if (manager->mRunning && manager->mFrameCallback) { + manager->mFrameCallback(buffer, totalSize, width, height, timestamp); + } + + delete[] buffer; + AImage_delete(image); +} \ No newline at end of file diff --git a/app/src/main/cpp/camera_manager.h b/app/src/main/cpp/camera_manager.h new file mode 100644 index 0000000..737ba9e --- /dev/null +++ b/app/src/main/cpp/camera_manager.h @@ -0,0 +1,49 @@ +#ifndef CAMERA_MANAGER_H +#define CAMERA_MANAGER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class CameraManager { +public: + using FrameCallback = std::function; + + CameraManager(); + ~CameraManager(); + + bool initialize(); + bool openCamera(const char* cameraId); + bool startCapture(int width, int height, FrameCallback callback); + void stopCapture(); + + std::vector getAvailableCameras(); + +private: + ACameraManager* mCameraManager = nullptr; + ACameraDevice* mCameraDevice = nullptr; + ACameraCaptureSession* mCaptureSession = nullptr; + ACameraOutputTarget* mOutputTarget = nullptr; + ACaptureRequest* mCaptureRequest = nullptr; + AImageReader* mImageReader = nullptr; + + FrameCallback mFrameCallback; + std::mutex mMutex; + std::condition_variable mCondVar; + bool mRunning = false; + + static void onDeviceDisconnected(void* context, ACameraDevice* device); + static void onDeviceError(void* context, ACameraDevice* device, int error); + static void onSessionClosed(void* context, ACameraCaptureSession* session); + static void onSessionReady(void* context, ACameraCaptureSession* session); + static void onSessionActive(void* context, ACameraCaptureSession* session); + static void imageCallback(void* context, AImageReader* reader); +}; + +#endif // CAMERA_MANAGER_H \ No newline at end of file diff --git a/app/src/main/cpp/encoder_manager.cpp b/app/src/main/cpp/encoder_manager.cpp new file mode 100644 index 0000000..2330494 --- /dev/null +++ b/app/src/main/cpp/encoder_manager.cpp @@ -0,0 +1,165 @@ +#include "encoder_manager.h" +#include +#include + +#define LOG_TAG "EncoderManager" +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) + +EncoderManager::EncoderManager() {} + +EncoderManager::~EncoderManager() { + stop(); +} + +bool EncoderManager::initialize(int width, int height, int bitrate, int frameRate, EncodedFrameCallback callback) { + mWidth = width; + mHeight = height; + mBitrate = bitrate; + mFrameRate = frameRate; + mCallback = callback; + + // Create H.264 encoder + mCodec = AMediaCodec_createEncoderByType("video/avc"); + if (!mCodec) { + LOGE("Failed to create H.264 encoder"); + return false; + } + + // Configure encoder + AMediaFormat* format = AMediaFormat_new(); + AMediaFormat_setString(format, AMEDIAFORMAT_KEY_MIME, "video/avc"); + AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_WIDTH, width); + AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_HEIGHT, height); + AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_BIT_RATE, bitrate); + AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_FRAME_RATE, frameRate); + AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_I_FRAME_INTERVAL, 1); // Key frame every second + AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_COLOR_FORMAT, 21); // COLOR_FormatYUV420SemiPlanar + + media_status_t status = AMediaCodec_configure( + mCodec, format, nullptr, nullptr, AMEDIACODEC_CONFIGURE_FLAG_ENCODE); + + AMediaFormat_delete(format); + + if (status != AMEDIA_OK) { + LOGE("Failed to configure encoder: %d", status); + AMediaCodec_delete(mCodec); + mCodec = nullptr; + return false; + } + + // Start encoder + status = AMediaCodec_start(mCodec); + if (status != AMEDIA_OK) { + LOGE("Failed to start encoder: %d", status); + AMediaCodec_delete(mCodec); + mCodec = nullptr; + return false; + } + + mRunning = true; + + // Start output processing thread + mOutputThread = std::thread(&EncoderManager::outputLoop, this); + + return true; +} + +bool EncoderManager::encode(uint8_t* yuvData, size_t dataSize, int64_t presentationTimeUs) { + if (!mRunning || !mCodec) { + return false; + } + + // Get input buffer index with timeout + ssize_t inputBufferIndex = AMediaCodec_dequeueInputBuffer(mCodec, 10000); + if (inputBufferIndex < 0) { + LOGE("Failed to get input buffer: %zd", inputBufferIndex); + return false; + } + + // Get input buffer and its size + size_t inputBufferSize; + uint8_t* inputBuffer = AMediaCodec_getInputBuffer(mCodec, inputBufferIndex, &inputBufferSize); + if (!inputBuffer) { + LOGE("Failed to get input buffer pointer"); + return false; + } + + // Make sure our data fits in the buffer + size_t toCopy = std::min(dataSize, inputBufferSize); + memcpy(inputBuffer, yuvData, toCopy); + + // Queue the input buffer with timestamp + media_status_t status = AMediaCodec_queueInputBuffer( + mCodec, inputBufferIndex, 0, toCopy, presentationTimeUs, 0); + + if (status != AMEDIA_OK) { + LOGE("Failed to queue input buffer: %d", status); + return false; + } + + return true; +} + +void EncoderManager::stop() { + if (mRunning) { + mRunning = false; + + if (mOutputThread.joinable()) { + mOutputThread.join(); + } + + if (mCodec) { + AMediaCodec_stop(mCodec); + AMediaCodec_delete(mCodec); + mCodec = nullptr; + } + } +} + +void EncoderManager::outputLoop() { + AMediaCodecBufferInfo bufferInfo; + + while (mRunning) { + // Dequeue output buffer with timeout + ssize_t outputBufferIndex = AMediaCodec_dequeueOutputBuffer(mCodec, &bufferInfo, 10000); + + if (outputBufferIndex >= 0) { + // Get output buffer + size_t outputBufferSize; + uint8_t* outputBuffer = AMediaCodec_getOutputBuffer(mCodec, outputBufferIndex, &outputBufferSize); + + if (outputBuffer && bufferInfo.size > 0 && mCallback) { + // Determine if it's a key frame + bool isKeyFrame = (bufferInfo.flags & 1) != 0; + + // Copy encoded data to a new buffer + uint8_t* data = new uint8_t[bufferInfo.size]; + memcpy(data, outputBuffer + bufferInfo.offset, bufferInfo.size); + + // Prepare frame and send via callback + EncodedFrame frame; + frame.data = data; + frame.size = bufferInfo.size; + frame.presentationTimeUs = bufferInfo.presentationTimeUs; + frame.isKeyFrame = isKeyFrame; + + mCallback(frame); + + delete[] data; + } + + // Release the output buffer + AMediaCodec_releaseOutputBuffer(mCodec, outputBufferIndex, false); + } else if (outputBufferIndex == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) { + // Format changed - could extract codec specific data here if needed + AMediaFormat* format = AMediaCodec_getOutputFormat(mCodec); + AMediaFormat_delete(format); + } else if (outputBufferIndex == AMEDIACODEC_INFO_TRY_AGAIN_LATER) { + // No output available yet - just continue + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } else { + LOGE("Unexpected output buffer index: %zd", outputBufferIndex); + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/encoder_manager.h b/app/src/main/cpp/encoder_manager.h new file mode 100644 index 0000000..969f27b --- /dev/null +++ b/app/src/main/cpp/encoder_manager.h @@ -0,0 +1,44 @@ +#ifndef ENCODER_MANAGER_H +#define ENCODER_MANAGER_H + +#include +#include +#include +#include +#include +#include +#include + +struct EncodedFrame { + uint8_t* data; + size_t size; + int64_t presentationTimeUs; + bool isKeyFrame; +}; + +class EncoderManager { +public: + using EncodedFrameCallback = std::function; + + EncoderManager(); + ~EncoderManager(); + + bool initialize(int width, int height, int bitrate, int frameRate, EncodedFrameCallback callback); + bool encode(uint8_t* yuvData, size_t dataSize, int64_t presentationTimeUs); + void stop(); + +private: + AMediaCodec* mCodec = nullptr; + int mWidth = 0; + int mHeight = 0; + int mBitrate = 0; + int mFrameRate = 0; + + EncodedFrameCallback mCallback; + std::atomic mRunning{false}; + + std::thread mOutputThread; + void outputLoop(); +}; + +#endif // ENCODER_MANAGER_H \ No newline at end of file diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp new file mode 100644 index 0000000..ea06eb0 --- /dev/null +++ b/app/src/main/cpp/native-lib.cpp @@ -0,0 +1,280 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +// #define USING_MULTI_CAMS + +#ifdef USING_FFMPEG +extern "C" { +#include +} +#endif + +#include "camera_manager.h" +#include "encoder_manager.h" +#include "rtsp_streamer.h" +#include "Utils.h" + +#define TAG "CAM2RTSP" +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__) +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__) + + +// Global variables +static std::unique_ptr gCameraManager; +static std::unique_ptr gEncoderManager; +static std::unique_ptr gRtspStreamer; + +static bool gIsRunning = false; +static int gWidth = 1280; +static int gHeight = 720; +static int gFps = 30; +static int gBitrate = 2000000; // 2 Mbps +static std::string gRtspUrl; + +// Frame processing callback +void onFrameEncoded(const EncodedFrame& frame) { + if (gRtspStreamer) { + gRtspStreamer->sendFrame(frame); + } +} + +// Camera frame callback +void onCameraFrame(uint8_t* data, size_t size, int32_t width, int32_t height, int64_t timestamp) { + if (gEncoderManager) { + // YUV420 conversion to NV12 might be needed depending on camera format + uint8_t* nv12Data = new uint8_t[size]; + utils::YUV420ToNV12(data, nv12Data, width, height); + + gEncoderManager->encode(nv12Data, size, timestamp); + + delete[] nv12Data; + } +} + + +void ffmpeg_log_callback(void *ptr, int level, const char *fmt, va_list vl) { + // Map FFmpeg log levels to Android log levels + int android_log_level; + switch (level) { + case AV_LOG_PANIC: + case AV_LOG_FATAL: + android_log_level = ANDROID_LOG_FATAL; + break; + case AV_LOG_ERROR: + android_log_level = ANDROID_LOG_ERROR; + break; + case AV_LOG_WARNING: + android_log_level = ANDROID_LOG_WARN; + break; + case AV_LOG_INFO: + android_log_level = ANDROID_LOG_INFO; + break; + case AV_LOG_VERBOSE: + android_log_level = ANDROID_LOG_VERBOSE; + break; + case AV_LOG_DEBUG: + case AV_LOG_TRACE: + android_log_level = ANDROID_LOG_DEBUG; + break; + default: + android_log_level = ANDROID_LOG_INFO; + break; + } + + // Format the log message + char log_message[1024]; + vsnprintf(log_message, sizeof(log_message), fmt, vl); + + if (android_log_level < AV_LOG_VERBOSE ) + { + // Send the log message to logcat + __android_log_print(android_log_level, "FFmpeg", "%s", log_message); + } + +} + + +jint JNI_OnLoad(JavaVM* vm, void* reserved) +{ + JNIEnv* env = NULL; + jint result = -1; + + // 在 JNI_OnLoad 或其他初始化函数中注册 +#if 0 + signal(SIGSEGV, sighandler); +#endif + +#if defined(JNI_VERSION_1_6) + if (result==-1 && vm->GetEnv((void**)&env, JNI_VERSION_1_6) == JNI_OK) + { + result = JNI_VERSION_1_6; + } +#endif +#if defined(JNI_VERSION_1_4) + if (result==-1 && vm->GetEnv((void**)&env, JNI_VERSION_1_4) == JNI_OK) + { + result = JNI_VERSION_1_4; + } +#endif +#if defined(JNI_VERSION_1_2) + if (result==-1 && vm->GetEnv((void**)&env, JNI_VERSION_1_2) == JNI_OK) + { + result = JNI_VERSION_1_2; + } +#endif + + if(result == -1 || env == NULL) + { + return JNI_FALSE; + } + + // curl_global_init(CURL_GLOBAL_ALL); + +#ifdef USING_FFMPEG + // Initialize FFmpeg +#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 9, 100) + av_register_all(); +#endif + + avformat_network_init(); + +#ifndef NDEBUG + // Set the custom log callback + av_log_set_level(AV_LOG_INFO); + av_log_set_callback(ffmpeg_log_callback); + + // av_log(NULL, AV_LOG_INFO, "Testing FFmpeg logging from JNI_OnLoad"); +#endif + +#endif + + return result; +} + +JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved) +{ +// curl_global_cleanup(); +#ifdef USING_FFMPEG +#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 9, 100) + av_unregister_all(); +#endif + avformat_network_deinit(); +#endif + + +} + + + +extern "C" JNIEXPORT jint JNICALL +Java_com_xypower_dblstreams_MainActivity_startPlayback( + JNIEnv* env, jobject pThis) { + if (gIsRunning) { + LOGI("Streaming already running"); + return JNI_TRUE; + } + + // Get RTSP URL + gRtspUrl = "rtsp://61.169.135.146:1554/live/11"; + + gWidth = 720; + gHeight = 480; + gFps = 15; + gBitrate = 2048*1024; + + LOGI("Starting streaming: %s (%dx%d @ %dfps)", gRtspUrl.c_str(), gWidth, gHeight, gFps); + + // Initialize RTSP streamer + gRtspStreamer = std::make_unique(); + if (!gRtspStreamer->initialize(gRtspUrl, gWidth, gHeight, gFps)) { + LOGE("Failed to initialize RTSP streamer"); + return JNI_FALSE; + } + + // Initialize encoder + gEncoderManager = std::make_unique(); + if (!gEncoderManager->initialize(gWidth, gHeight, gBitrate, gFps, onFrameEncoded)) { + LOGE("Failed to initialize encoder"); + return JNI_FALSE; + } + + // Initialize camera + gCameraManager = std::make_unique(); + if (!gCameraManager->initialize()) { + LOGE("Failed to initialize camera"); + return JNI_FALSE; + } + + // Get available cameras + auto cameras = gCameraManager->getAvailableCameras(); + if (cameras.empty()) { + LOGE("No cameras available"); + return JNI_FALSE; + } + + // Open first available camera (usually back camera) + if (!gCameraManager->openCamera(cameras[0].c_str())) { + LOGE("Failed to open camera"); + return JNI_FALSE; + } + + // Start camera capture + if (!gCameraManager->startCapture(gWidth, gHeight, onCameraFrame)) { + LOGE("Failed to start camera capture"); + return JNI_FALSE; + } + + gIsRunning = true; + return JNI_TRUE; +} + + +extern "C" JNIEXPORT jint JNICALL +Java_com_xypower_dblstreams_MainActivity_startRtmpPlayback( + JNIEnv* env, jobject pThis) { + + + return 0; +} + +extern "C" JNIEXPORT void JNICALL +Java_com_xypower_dblstreams_MainActivity_stopPlayback( + JNIEnv* env, jobject pThis) { + if (!gIsRunning) { + return; + } + + LOGI("Stopping streaming"); + + // Stop and clean up camera + if (gCameraManager) { + gCameraManager->stopCapture(); + gCameraManager.reset(); + } + + // Stop and clean up encoder + if (gEncoderManager) { + gEncoderManager->stop(); + gEncoderManager.reset(); + } + + // Stop and clean up RTSP streamer + if (gRtspStreamer) { + gRtspStreamer->stop(); + gRtspStreamer.reset(); + } + + gIsRunning = false; +} \ No newline at end of file diff --git a/app/src/main/cpp/rtmp_streamer.cpp b/app/src/main/cpp/rtmp_streamer.cpp new file mode 100644 index 0000000..f146a79 --- /dev/null +++ b/app/src/main/cpp/rtmp_streamer.cpp @@ -0,0 +1,390 @@ +#include "rtmp_streamer.h" +#include +#include +#include + +#include + +// FFmpeg 4.4.5 includes +extern "C" { +#include +#include +#include +#include +#include +} + + +// Verify we're using FFmpeg 4.4.5 +#if LIBAVFORMAT_VERSION_MAJOR != 58 || LIBAVFORMAT_VERSION_MINOR != 76 +#warning "This code is optimized for FFmpeg 4.4.5 (libavformat 58.76.100)" +#endif + +void debug_save_stream(const uint8_t* data, size_t size, bool is_keyframe, bool is_converted) { + static FILE* raw_file = NULL; + static FILE* converted_file = NULL; + + if (!raw_file) raw_file = fopen("/sdcard/rtmp_raw.h264", "wb"); + if (!converted_file) converted_file = fopen("/sdcard/rtmp_converted.h264", "wb"); + + FILE* target = is_converted ? converted_file : raw_file; + if (target) { + if (is_keyframe) { + uint8_t marker[4] = {0, 0, 0, 0}; // Visual marker for keyframes + fwrite(marker, 1, 4, target); + } + fwrite(data, 1, size, target); + fflush(target); + } +} + +// Add this function to convert Annex B to AVCC format +bool convert_annexb_to_avcc(const uint8_t* annexb_data, size_t annexb_size, + uint8_t** avcc_data, size_t* avcc_size) { + // Count NAL units and calculate required size + size_t total_size = 0; + int nal_count = 0; + + for (size_t i = 0; i < annexb_size - 3; i++) { + // Find start code + if ((annexb_data[i] == 0 && annexb_data[i+1] == 0 && annexb_data[i+2] == 0 && annexb_data[i+3] == 1) || + (annexb_data[i] == 0 && annexb_data[i+1] == 0 && annexb_data[i+2] == 1)) { + nal_count++; + } + } + + // Allocate output buffer (estimate size) + *avcc_data = (uint8_t*)malloc(annexb_size + nal_count*4); + uint8_t* out = *avcc_data; + *avcc_size = 0; + + // Convert each NAL unit + for (size_t i = 0; i < annexb_size;) { + // Find start code + if ((i+3 < annexb_size && annexb_data[i] == 0 && annexb_data[i+1] == 0 && + annexb_data[i+2] == 0 && annexb_data[i+3] == 1) || + (i+2 < annexb_size && annexb_data[i] == 0 && annexb_data[i+1] == 0 && + annexb_data[i+2] == 1)) { + + int start_code_size = (annexb_data[i+2] == 1) ? 3 : 4; + i += start_code_size; + + // Find next start code + size_t j = i; + while (j < annexb_size - 3) { + if ((annexb_data[j] == 0 && annexb_data[j+1] == 0 && annexb_data[j+2] == 1) || + (annexb_data[j] == 0 && annexb_data[j+1] == 0 && + annexb_data[j+2] == 0 && annexb_data[j+3] == 1)) { + break; + } + j++; + } + + // NAL unit size + size_t nal_size = j - i; + + // Write length prefix (4 bytes) + *out++ = (nal_size >> 24) & 0xff; + *out++ = (nal_size >> 16) & 0xff; + *out++ = (nal_size >> 8) & 0xff; + *out++ = nal_size & 0xff; + + // Copy NAL unit + memcpy(out, annexb_data + i, nal_size); + out += nal_size; + *avcc_size += nal_size + 4; + + i = j; + } else { + i++; + } + } + + return true; +} + +bool rtmp_streamer_init(RtspStreamer* streamer, const char* rtmpUrl, + int width, int height, int bitrate, int frameRate, + const uint8_t* sps, size_t spsSize, + const uint8_t* pps, size_t ppsSize) { + + + // Check protocol support + AVOutputFormat* outfmt = av_guess_format("rtmp", NULL, NULL); + if (!outfmt) { + __android_log_print(ANDROID_LOG_ERROR, "FFmpeg", "RTMP protocol not supported in this FFmpeg build!"); + } else { + __android_log_print(ANDROID_LOG_INFO, "FFmpeg", "RTMP format supported"); + } + + // List available protocols + void *opaque = NULL; + const char *name = NULL; + __android_log_print(ANDROID_LOG_INFO, "FFmpeg", "Available output protocols:"); + while ((name = avio_enum_protocols(&opaque, 1))) { + __android_log_print(ANDROID_LOG_INFO, "FFmpeg", " %s", name); + } + + memset(streamer, 0, sizeof(RtspStreamer)); + + streamer->width = width; + streamer->height = height; + streamer->bitrate = bitrate; + streamer->frameRate = frameRate; + streamer->rtspUrl = strdup(rtmpUrl); // Keep the field name for now + + // Allocate output format context + int ret = avformat_alloc_output_context2(&streamer->formatCtx, NULL, "flv", rtmpUrl); + if (ret < 0 || !streamer->formatCtx) { + fprintf(stderr, "Could not create output context, error: %d\n", ret); + return false; + } + + // Find H.264 encoder + const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_H264); + if (!codec) { + fprintf(stderr, "Could not find H.264 encoder\n"); + return false; + } + + // Create video stream + streamer->stream = avformat_new_stream(streamer->formatCtx, NULL); + if (!streamer->stream) { + fprintf(stderr, "Could not create video stream\n"); + return false; + } + + streamer->stream->id = streamer->formatCtx->nb_streams - 1; + + // Initialize codec context + streamer->codecCtx = avcodec_alloc_context3(codec); + if (!streamer->codecCtx) { + fprintf(stderr, "Could not allocate codec context\n"); + return false; + } + + // Set codec parameters + streamer->codecCtx->codec_id = AV_CODEC_ID_H264; + streamer->codecCtx->codec_type = AVMEDIA_TYPE_VIDEO; + streamer->codecCtx->width = width; + streamer->codecCtx->height = height; + streamer->codecCtx->pix_fmt = AV_PIX_FMT_YUV420P; + streamer->codecCtx->bit_rate = bitrate; + streamer->codecCtx->time_base.num = 1; + streamer->codecCtx->time_base.den = frameRate; + + // Use h264_passthrough as we'll receive pre-encoded H.264 data + streamer->codecCtx->codec_tag = 0; + + // ADD THIS CODE - Create extradata with SPS/PPS in AVCC format (required by RTMP) + size_t extradata_size = 8 + spsSize + 3 + ppsSize; + streamer->codecCtx->extradata = (uint8_t*)av_malloc(extradata_size + AV_INPUT_BUFFER_PADDING_SIZE); + if (!streamer->codecCtx->extradata) { + fprintf(stderr, "Failed to allocate extradata\n"); + return false; + } + memset(streamer->codecCtx->extradata, 0, extradata_size + AV_INPUT_BUFFER_PADDING_SIZE); + + if (!sps || spsSize < 4) { + __android_log_print(ANDROID_LOG_ERROR, "RTMP", "Invalid SPS: %p, size: %zu", sps, spsSize); + return false; + } + + if (!pps || ppsSize < 1) { + __android_log_print(ANDROID_LOG_ERROR, "RTMP", "Invalid PPS: %p, size: %zu", pps, ppsSize); + return false; + } + + // Format extradata as AVCC (needed by RTMP) + uint8_t* p = streamer->codecCtx->extradata; + *p++ = 1; // version + *p++ = sps[1]; // profile + *p++ = sps[2]; // profile compat + *p++ = sps[3]; // level + *p++ = 0xff; // 6 bits reserved + 2 bits NAL size length - 1 (3) + *p++ = 0xe1; // 3 bits reserved + 5 bits number of SPS (1) + + // SPS length and data + *p++ = (spsSize >> 8) & 0xff; + *p++ = spsSize & 0xff; + memcpy(p, sps, spsSize); + p += spsSize; + + // Number of PPS + *p++ = 1; + + // PPS length and data + *p++ = (ppsSize >> 8) & 0xff; + *p++ = ppsSize & 0xff; + memcpy(p, pps, ppsSize); + + streamer->codecCtx->extradata_size = extradata_size; + // END OF ADDITION + + // Use h264_passthrough as we'll receive pre-encoded H.264 data + streamer->codecCtx->codec_tag = 0; + + // Copy parameters to stream + ret = avcodec_parameters_from_context(streamer->stream->codecpar, streamer->codecCtx); + if (ret < 0) { + fprintf(stderr, "Could not copy codec parameters to stream, error: %d\n", ret); + return false; + } + + // Set stream timebase + streamer->stream->time_base = streamer->codecCtx->time_base; + + // Some formats want stream headers to be separate + if (streamer->formatCtx->oformat->flags & AVFMT_GLOBALHEADER) { + streamer->codecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + } + + // In rtmp_streamer_init, update the options dictionary: + AVDictionary* options = NULL; + av_dict_set(&options, "flvflags", "no_duration_filesize", 0); + av_dict_set(&options, "tune", "zerolatency", 0); + av_dict_set(&options, "preset", "ultrafast", 0); + av_dict_set(&options, "fflags", "nobuffer", 0); + av_dict_set(&options, "rw_timeout", "8000000", 0); // 8 second timeout (ZLM default) + av_dict_set(&options, "buffer_size", "32768", 0); // Larger buffer for stability + av_dict_set(&options, "flush_packets", "1", 0); // Force packet flushing + + // Add before avio_open2 + av_dump_format(streamer->formatCtx, 0, rtmpUrl, 1); + + // Open output URL + ret = avio_open2(&streamer->formatCtx->pb, rtmpUrl, AVIO_FLAG_WRITE, NULL, &options); + if (ret < 0) { + char err_buf[AV_ERROR_MAX_STRING_SIZE] = {0}; + av_strerror(ret, err_buf, AV_ERROR_MAX_STRING_SIZE); + fprintf(stderr, "Could not open output URL '%s', error: %s\n", rtmpUrl, err_buf); + return false; + } + + // Write stream header + ret = avformat_write_header(streamer->formatCtx, &options); + if (ret < 0) { + char err_buf[AV_ERROR_MAX_STRING_SIZE] = {0}; + av_strerror(ret, err_buf, AV_ERROR_MAX_STRING_SIZE); + fprintf(stderr, "Error writing header: %s\n", err_buf); + return false; + } + + // Allocate packet + streamer->packet = av_packet_alloc(); + if (!streamer->packet) { + fprintf(stderr, "Could not allocate packet\n"); + return false; + } + + streamer->isConnected = true; + streamer->startTime = av_gettime(); + streamer->frameCount = 0; + + return true; +} + + + +bool rtmp_streamer_send_h264(RtspStreamer* streamer, const uint8_t* data, + size_t dataLength, int64_t pts, bool isKeyFrame) { + if (!streamer || !streamer->isConnected || !data || dataLength == 0) { + return false; + } + + static FILE* debug_file = NULL; + if (!debug_file) { + debug_file = fopen("/sdcard/com.xypower.mpapp/tmp/rtmp_debug.h264", "wb"); + } + + if (debug_file) { + fwrite(data, 1, dataLength, debug_file); + fflush(debug_file); + } + + // Convert Annex B (start code) format to AVCC (length prefix) format + // RTMP requires AVCC format for H.264 data + uint8_t* avcc_data = NULL; + size_t avcc_size = 0; + + if (!convert_annexb_to_avcc(data, dataLength, &avcc_data, &avcc_size)) { + __android_log_print(ANDROID_LOG_ERROR, "RTMP", "Failed to convert H.264 to AVCC format"); + return false; + } + + // Log frame info + __android_log_print(ANDROID_LOG_VERBOSE, "RTMP", "Sending frame: %zu bytes, keyframe: %d", + avcc_size, isKeyFrame ? 1 : 0); + + // Reset packet + av_packet_unref(streamer->packet); + + // Copy encoded data to packet buffer (use converted AVCC data) + uint8_t* buffer = (uint8_t*)av_malloc(avcc_size); + if (!buffer) { + __android_log_print(ANDROID_LOG_ERROR, "RTMP", "Failed to allocate memory for packet data"); + free(avcc_data); + return false; + } + + memcpy(buffer, avcc_data, avcc_size); + free(avcc_data); // Free the converted data + + // Set up packet with AVCC formatted data + streamer->packet->data = buffer; + streamer->packet->size = avcc_size; + + // RTMP timestamp handling + if (pts == 0) { + pts = av_gettime() - streamer->startTime; + } + + // Convert to stream time_base for FLV/RTMP + // ZLMediaKit requires strictly monotonic timestamps + static int64_t last_dts = 0; + + // Calculate timestamp in milliseconds + int64_t timestamp_ms = pts ? pts / 1000 : (av_gettime() - streamer->startTime) / 1000; + + // Convert to stream timebase + int64_t ts_in_stream_tb = av_rescale_q(timestamp_ms, + (AVRational){1, 1000}, // millisecond timebase + streamer->stream->time_base); + + // Ensure monotonically increasing timestamps + if (ts_in_stream_tb <= last_dts) { + ts_in_stream_tb = last_dts + 1; + } + last_dts = ts_in_stream_tb; + +// Set both PTS and DTS + streamer->packet->pts = ts_in_stream_tb; + streamer->packet->dts = ts_in_stream_tb; + streamer->packet->duration = av_rescale_q(1, + (AVRational){1, streamer->frameRate}, + streamer->stream->time_base); + + // Set key frame flag - especially important for RTMP + // In rtmp_streamer_send_h264, enhance keyframe logging: + if (isKeyFrame) { + streamer->packet->flags |= AV_PKT_FLAG_KEY; + __android_log_print(ANDROID_LOG_INFO, "RTMP", + "Sending keyframe (size: %zu, pts: %lld)", avcc_size, streamer->packet->pts); + } + + streamer->packet->stream_index = streamer->stream->index; + + // Write packet + int ret = av_interleaved_write_frame(streamer->formatCtx, streamer->packet); + av_free(buffer); // Free allocated buffer + + if (ret < 0) { + char err_buf[AV_ERROR_MAX_STRING_SIZE] = {0}; + av_strerror(ret, err_buf, AV_ERROR_MAX_STRING_SIZE); + __android_log_print(ANDROID_LOG_ERROR, "RTMP", "Error writing frame: %s", err_buf); + return false; + } + + streamer->frameCount++; + return true; +} diff --git a/app/src/main/cpp/rtmp_streamer.h b/app/src/main/cpp/rtmp_streamer.h new file mode 100644 index 0000000..b0c4cc4 --- /dev/null +++ b/app/src/main/cpp/rtmp_streamer.h @@ -0,0 +1,48 @@ +#ifndef RTSP_STREAMER_H +#define RTSP_STREAMER_H + +extern "C" { +#include +#include +#include +#include +} + +#include +#include +#include +#include +#include +#include + +#include "camera_manager.h" + +class RtspStreamer { +public: + RtspStreamer(); + ~RtspStreamer(); + + bool initialize(const std::string& url, int width, int height, int fps); + bool sendFrame(const EncodedFrame& frame); + void stop(); + +private: + std::string mUrl; + AVFormatContext* mFormatCtx = nullptr; + AVStream* mVideoStream = nullptr; + AVCodecContext* mCodecCtx = nullptr; + AVPacket* mPacket = nullptr; + + int64_t mStartTime = 0; + std::atomic mRunning{false}; + + std::queue mFrameQueue; + std::mutex mQueueMutex; + std::condition_variable mQueueCond; + std::thread mStreamThread; + + void extractH264Parameters(const uint8_t* data, size_t size, uint8_t** sps, size_t* spsSize, uint8_t** pps, size_t* ppsSize); + void streamLoop(); +}; + +#endif // RTSP_STREAMER_H \ No newline at end of file diff --git a/app/src/main/cpp/rtsp_streamer.cpp b/app/src/main/cpp/rtsp_streamer.cpp new file mode 100644 index 0000000..17f5558 --- /dev/null +++ b/app/src/main/cpp/rtsp_streamer.cpp @@ -0,0 +1,265 @@ +#include "rtsp_streamer.h" +#include +#include + +#define LOG_TAG "RtspStreamer" +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) + +// Include the encoder_manager.h to get the EncodedFrame structure +#include "encoder_manager.h" + +// Define key frame flag constant +#define BUFFER_FLAG_KEY_FRAME 1 + +RtspStreamer::RtspStreamer() : mPacket(nullptr), mFormatCtx(nullptr), + mVideoStream(nullptr), mCodecCtx(nullptr), + mStartTime(0), mRunning(false) {} + +RtspStreamer::~RtspStreamer() { + stop(); +} + +bool RtspStreamer::initialize(const std::string& url, int width, int height, int fps) { + mUrl = url; + + // Initialize FFmpeg + avformat_network_init(); + + // Allocate format context + int ret = avformat_alloc_output_context2(&mFormatCtx, nullptr, "rtsp", url.c_str()); + if (ret < 0 || !mFormatCtx) { + char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0}; + av_strerror(ret, errbuf, AV_ERROR_MAX_STRING_SIZE); + LOGE("Could not allocate output format context: %s", errbuf); + return false; + } + + // Set RTSP options + av_dict_set(&mFormatCtx->metadata, "rtsp_transport", "tcp", 0); // Use TCP for more reliability + mFormatCtx->oformat->flags |= AVFMT_FLAG_NONBLOCK; + + // Find the H.264 encoder + const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_H264); + if (!codec) { + LOGE("Could not find H.264 encoder"); + return false; + } + + // Create video stream + mVideoStream = avformat_new_stream(mFormatCtx, nullptr); + if (!mVideoStream) { + LOGE("Could not create video stream"); + return false; + } + mVideoStream->id = mFormatCtx->nb_streams - 1; + + // Set stream parameters + AVCodecParameters* codecpar = mVideoStream->codecpar; + codecpar->codec_id = AV_CODEC_ID_H264; + codecpar->codec_type = AVMEDIA_TYPE_VIDEO; + codecpar->width = width; + codecpar->height = height; + codecpar->format = AV_PIX_FMT_YUV420P; + codecpar->bit_rate = 2000000; // 2 Mbps + + // Stream timebase (90kHz is standard for H.264 in RTSP) + mVideoStream->time_base = (AVRational){1, 90000}; + + // Open output URL + if (!(mFormatCtx->oformat->flags & AVFMT_NOFILE)) { + ret = avio_open(&mFormatCtx->pb, url.c_str(), AVIO_FLAG_WRITE); + if (ret < 0) { + char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0}; + av_strerror(ret, errbuf, AV_ERROR_MAX_STRING_SIZE); + LOGE("Could not open output URL: %s, error: %s", url.c_str(), errbuf); + return false; + } + } + + // Write stream header + AVDictionary* opts = nullptr; + av_dict_set(&opts, "rtsp_transport", "tcp", 0); + ret = avformat_write_header(mFormatCtx, &opts); + if (ret < 0) { + char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0}; + av_strerror(ret, errbuf, AV_ERROR_MAX_STRING_SIZE); + LOGE("Error writing header: %s", errbuf); + return false; + } + av_dict_free(&opts); + + // Allocate packet + mPacket = av_packet_alloc(); + if (!mPacket) { + LOGE("Could not allocate packet"); + return false; + } + + mRunning = true; + mStartTime = av_gettime(); + + // Start streaming thread + mStreamThread = std::thread(&RtspStreamer::streamLoop, this); + + LOGI("RTSP streamer initialized successfully to %s", url.c_str()); + return true; +} + +bool RtspStreamer::sendFrame(const EncodedFrame& frame) { + if (!mRunning) { + return false; + } + + // Make a copy of the frame + EncodedFrame frameCopy; + frameCopy.size = frame.size; + frameCopy.presentationTimeUs = frame.presentationTimeUs; + frameCopy.isKeyFrame = frame.isKeyFrame; + + // Copy the data + frameCopy.data = new uint8_t[frame.size]; + memcpy(frameCopy.data, frame.data, frame.size); + + // Add the frame to queue + { + std::lock_guard lock(mQueueMutex); + mFrameQueue.push(frameCopy); + } + + mQueueCond.notify_one(); + return true; +} + +void RtspStreamer::stop() { + if (mRunning) { + mRunning = false; + + // Wake up streaming thread + mQueueCond.notify_all(); + + if (mStreamThread.joinable()) { + mStreamThread.join(); + } + + // Clean up frames in queue + { + std::lock_guard lock(mQueueMutex); + while (!mFrameQueue.empty()) { + EncodedFrame& frame = mFrameQueue.front(); + delete[] frame.data; + mFrameQueue.pop(); + } + } + + // Write trailer and close + if (mFormatCtx) { + if (mFormatCtx->pb) { + av_write_trailer(mFormatCtx); + } + + if (!(mFormatCtx->oformat->flags & AVFMT_NOFILE) && mFormatCtx->pb) { + avio_close(mFormatCtx->pb); + } + + avformat_free_context(mFormatCtx); + mFormatCtx = nullptr; + } + + if (mCodecCtx) { + avcodec_free_context(&mCodecCtx); + } + + if (mPacket) { + av_packet_free(&mPacket); + } + + LOGI("RTSP streamer stopped"); + } +} + +void RtspStreamer::streamLoop() { + bool firstFrame = true; + int64_t firstPts = 0; + + while (mRunning) { + EncodedFrame frame; + bool hasFrame = false; + + // Get frame from queue + { + std::unique_lock lock(mQueueMutex); + if (mFrameQueue.empty()) { + // Wait for new frame or stop signal + mQueueCond.wait_for(lock, std::chrono::milliseconds(100)); + continue; + } + + frame = mFrameQueue.front(); + mFrameQueue.pop(); + hasFrame = true; + } + + if (hasFrame) { + // Reset the packet + av_packet_unref(mPacket); + + // Save first timestamp for offset calculation + if (firstFrame) { + firstPts = frame.presentationTimeUs; + firstFrame = false; + } + + // Create a copy of the frame data that FFmpeg will manage + uint8_t* buffer = (uint8_t*)av_malloc(frame.size); + if (!buffer) { + LOGE("Failed to allocate buffer for frame"); + delete[] frame.data; // Free our copy + continue; + } + + // Copy frame data to the FFmpeg-managed buffer + memcpy(buffer, frame.data, frame.size); + + // We can now free our copy of the data + delete[] frame.data; + frame.data = nullptr; // Avoid accidental double-delete + + // Let FFmpeg manage the buffer + int ret = av_packet_from_data(mPacket, buffer, frame.size); + if (ret < 0) { + LOGE("Failed to create packet from data: %d", ret); + av_free(buffer); // Free FFmpeg buffer on error + continue; + } + + // Now mPacket owns the buffer, we don't need to free it manually + + // Offset timestamp by first frame for proper timing + int64_t pts = frame.presentationTimeUs - firstPts; + + // Convert to stream timebase (90kHz) + pts = av_rescale_q(pts, (AVRational){1, 1000000}, mVideoStream->time_base); + + // Set packet properties + mPacket->pts = pts; + mPacket->dts = pts; + mPacket->duration = 0; + mPacket->flags = frame.isKeyFrame ? AV_PKT_FLAG_KEY : 0; + mPacket->stream_index = mVideoStream->index; + + // Write packet + ret = av_interleaved_write_frame(mFormatCtx, mPacket); + if (ret < 0) { + char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0}; + av_strerror(ret, errbuf, AV_ERROR_MAX_STRING_SIZE); + LOGE("Error writing frame: %d (%s)", ret, errbuf); + + // Handle reconnection logic as before... + } + + // We don't need to delete frame.data here anymore - it's already been freed above + // and ownership of the buffer has been transferred to FFmpeg + } + } +} \ No newline at end of file diff --git a/app/src/main/cpp/rtsp_streamer.h b/app/src/main/cpp/rtsp_streamer.h new file mode 100644 index 0000000..1e49548 --- /dev/null +++ b/app/src/main/cpp/rtsp_streamer.h @@ -0,0 +1,48 @@ +#ifndef RTSP_STREAMER_H +#define RTSP_STREAMER_H + +extern "C" { +#include +#include +#include +#include +} + +#include +#include +#include +#include +#include +#include + +#include "encoder_manager.h" + +class RtspStreamer { +public: + RtspStreamer(); + ~RtspStreamer(); + + bool initialize(const std::string& url, int width, int height, int fps); + bool sendFrame(const EncodedFrame& frame); + void stop(); + +private: + std::string mUrl; + AVFormatContext* mFormatCtx = nullptr; + AVStream* mVideoStream = nullptr; + AVCodecContext* mCodecCtx = nullptr; + AVPacket* mPacket = nullptr; + + int64_t mStartTime = 0; + std::atomic mRunning{false}; + + std::queue mFrameQueue; + std::mutex mQueueMutex; + std::condition_variable mQueueCond; + std::thread mStreamThread; + + void extractH264Parameters(const uint8_t* data, size_t size, uint8_t** sps, size_t* spsSize, uint8_t** pps, size_t* ppsSize); + void streamLoop(); +}; + +#endif // RTSP_STREAMER_H \ No newline at end of file diff --git a/app/src/main/java/com/xypower/dblstreams/MainActivity.java b/app/src/main/java/com/xypower/dblstreams/MainActivity.java new file mode 100644 index 0000000..995279d --- /dev/null +++ b/app/src/main/java/com/xypower/dblstreams/MainActivity.java @@ -0,0 +1,69 @@ +package com.xypower.dblstreams; + +import android.os.Bundle; +import android.view.View; +import android.widget.Button; + +import androidx.appcompat.app.AppCompatActivity; + + +public class MainActivity extends AppCompatActivity { + + static { + // 加载本地库(对应 libnative-lib.so) + System.loadLibrary("dblstreams"); + } + + private static final String TAG = "DualStreamingActivity"; + private static final int REQUEST_CAMERA_PERMISSION = 200; + + private native int startPlayback(); + + private native int startRtmpPlayback(); + private native int stopPlayback(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + Button btnStop = (Button)findViewById(R.id.btnStopStreaming); + btnStop.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + stopPlayback(); + } + }); + + // 初始化UI + Thread th = new Thread(new Runnable() { + @Override + public void run() { + startPlayback(); + } + }); + th.start(); + + + } + + + @Override + protected void onResume() { + super.onResume(); + // 摄像头只有在开始推流时才会打开,这里不需要额外操作 + } + + @Override + protected void onPause() { + // 如果Activity暂停,停止推流 + super.onPause(); + } + + @Override + protected void onDestroy() { + // 确保释放资源 + + super.onDestroy(); + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..09bf288 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,37 @@ + + + + + +