大牛直播 Android SDK 接入 Flutter Demo
实现裁图自定义渲染

gavin.chen 4e23716765 解决内存泄漏问题 1 рік тому
.vscode 0dd4e9a260 init commit 1 рік тому
android 0dd4e9a260 init commit 1 рік тому
assets 0dd4e9a260 init commit 1 рік тому
ios 4e23716765 解决内存泄漏问题 1 рік тому
lib 4e23716765 解决内存泄漏问题 1 рік тому
test 0dd4e9a260 init commit 1 рік тому
web 0dd4e9a260 init commit 1 рік тому
.gitignore 60c333dcc1 init repo 1 рік тому
.metadata 0dd4e9a260 init commit 1 рік тому
README.md 0c3cb65156 iOS SDK 已加入项目,还未直接调用,编译可以通过 1 рік тому
package.bat 0dd4e9a260 init commit 1 рік тому
pubspec.yaml 0dd4e9a260 init commit 1 рік тому

README.md

在 Flutter 集成大牛 RTMP Android SDK

  • 首先设置一个最小环境以供测试用 flyinsono-min

编译 Android Demo 包注意事项

  • 项目路径不能存在中文
  • 要更新 app 目录下的 build.gradle 更新 gradle 版本
  • 要修改 AndroidManifest.xml 下的 app_name 修改为授权允许的 FLYINSONO

代码集成

确保 SmartPlayerJniV2.java 放到 com.daniulive.smartplayer 包名下(可在其他 包名下调用);

  • Smartavengine.jar 加入到工程;
  • 拷贝 SmartPlayerV2\app\src\main\jniLibs\armeabi-v7a、SmartPlayerV2\app\src\main\jniLibs\arm64-v8a、SmartPlayerV2\app\src\main\jniLibs\x86SmartPlayerV2\app\src\main\jniLibs\x86_64libSmartPlayer.so 到工程
  1. Smartavengine.jar添加到工程中,具体操作如下:

    • 创建一个名为libs的文件夹,将其放在您的Android原生项目的android\app目录下;
    • Smartavengine.jar文件复制到刚创建的libs文件夹;
    • android\app\build.gradle文件中添加以下代码:

      android {
      	//...
      	splits {
      		// Configures screen ABI split settings
      		abi {
      			// Enable ABI APK splits
      			enable true
      		
      			// Resets the list of ABIs that Gradle should create APKs for to none
      			reset()
      		
      			// Specifies a list of ABIs that Gradle should create APKs for
      			include 'armeabi-v7a', 'arm64-v8a' //select ABIs to build APKs for
      		
      			// Specify that we do not want to also generate a universal APK that includes all ABIs
      			universalApk true
      		}
      	}
      }
      //...
      dependencies {
      	//...
      	implementation files('libs/Smartavengine.jar')
      }
      

权限配置

android\app\src\main\AndroidManifest.xml 内增加

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

项目配置

android\app\src\main\kotlin\com\vinno\flyinsono\RTMPPlugin.kt 下写入 这里面主要是将 Flutter 自定义渲染在 Player 初始化时传入 Player

package com.vinno.flyinsono

import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.EventChannel.EventSink;
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import com.videoengine.NTExternalRender;
import com.videoengine.NTRenderer;
import com.videoengine.NTSEIDataCallback;
import com.videoengine.NTUserDataCallback;
import java.nio.ByteBuffer;
import android.os.Handler
import android.os.Looper

import com.daniulive.smartplayer.SmartPlayer
import android.content.Context;

class RTMPPlugin : FlutterPlugin, MethodCallHandler {
    private lateinit var methodChannel: MethodChannel
    private lateinit var eventChannel: EventChannel
    private lateinit var smartPlayer: SmartPlayer
    private lateinit var eventSink: EventSink
    private lateinit var flutterExternalRender: FlutterExternalRender
    // private lateinit var currContext: Context


    override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        val currContext = flutterPluginBinding.applicationContext
        /// Flutter 渲染器
        flutterExternalRender = FlutterExternalRender()
        smartPlayer = SmartPlayer(currContext, flutterExternalRender)
        methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME)
        methodChannel.setMethodCallHandler(this)

        // 使用 EventChannel 添加回调函数,用于接收smartPlayer每一帧的图像数据
        eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, EVENT_CHANNEL_NAME)
        eventChannel.setStreamHandler(object : EventChannel.StreamHandler {
            override fun onListen(arguments: Any?, eventSink: EventSink) {
                this@RTMPPlugin.eventSink = eventSink
                println("⭐⭐⭐⭐ 设置事件监听 ⭐⭐⭐⭐")
            }
            override fun onCancel(arguments: Any?) {
                // smartPlayer.setExternalRender(null)
            }
        })
    }

    override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
        when (call.method) {
            "start" -> {
                startRTMP()
                result.success(null)
            }
            "stop" -> {
                stopRTMP()
                result.success(null)
            }
            else -> result.notImplemented()
        }
    }

    private fun startRTMP() {
        smartPlayer.onClickStart()
        println("Simulating RTMPSDK.initialize()")
        println("Simulating RTMPSDK.startStreaming()")
    }

    private fun stopRTMP() {
        smartPlayer.onClickStop()
        println("Simulating RTMPSDK.stopStreaming()")
        println("Simulating RTMPSDK.release()")
    }

    override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
        methodChannel.setMethodCallHandler(null)
    }

    companion object {
        private const val CHANNEL_NAME = "com.vinno.flyinsono.rtmp"
        private const val EVENT_CHANNEL_NAME = "com.vinno.flyinsono.rtmp.event"
    }

    inner class FlutterExternalRender : NTExternalRender {
        private var width_ = 0
        private var height_ = 0
        private var row_bytes_ = 0
        private var rgba_buffer_: ByteBuffer? = null

        override fun getNTFrameFormat(): Int {
            return 1
        }

        override fun onNTFrameSizeChanged(width: Int, height: Int) {
            width_ = width
            height_ = height
            row_bytes_ = width_ * 4
            rgba_buffer_ = ByteBuffer.allocateDirect(row_bytes_ * height_)
            println("onNTFrameSizeChanged" + width_ + " " + height_ + " " + row_bytes_ + "⭐")
        }

        override fun getNTPlaneByteBuffer(index: Int): ByteBuffer? {
            if (index == 0) {
                return rgba_buffer_;
            } else {
                return null
            }
        }

        override fun getNTPlanePerRowBytes(index: Int): Int {
            if (index == 0) {
                return row_bytes_
            } else {
                return 0
            }
        }

        override fun onNTRenderFrame(width: Int, height: Int, timestamp: Long) {
            if (rgba_buffer_ == null) return
            rgba_buffer_?.rewind()
            val rgba = ByteArray(row_bytes_*height_)
            rgba_buffer_?.get(rgba)
            // 使用 Handler 在主线程中执行 eventSink.success 方法
            Handler(Looper.getMainLooper()).post {
                this@RTMPPlugin.eventSink.success(rgba)
            }
        }
    }
}

android\app\src\main\kotlin\com\vinno\flyinsono\MainActivity.kt 内新增

...
import com.vinno.flyinsono.RTMPPlugin;
...
class MainActivity: FlutterActivity() {
	...
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        ...
        flutterEngine.plugins.add(RTMPPlugin())  // 注册 RTMPPlugin
    }
}

lib\views\temp\rtmp_android_method\rtmp_android.dart 内写入

import 'dart:async';
import 'package:flutter/services.dart';

class RTMPAndroidMethodChannel {
  static const MethodChannel _methodChannel =
      MethodChannel("com.vinno.flyinsono.rtmp");
  static const EventChannel _eventChannel =
      EventChannel('com.vinno.flyinsono.rtmp.event');
  static Stream<List<int>>? _imageStream;
  StreamController _streamController = StreamController();

  static final RTMPAndroidMethodChannel _singleton =
      RTMPAndroidMethodChannel._internal();

  factory RTMPAndroidMethodChannel() {
    return _singleton;
  }

  RTMPAndroidMethodChannel._internal() {
    _init();
  }

  Future _onMethodCall(MethodCall call) async {
    switch (call.method) {
      case "onFrameData":
        Uint8List frameData = call.arguments;
        _streamController.add(frameData);
        break;
      default:
        throw UnimplementedError("Method ${call.method} not implemented");
    }
  }

  _init() async {
    print("初始化移动端订阅");
    _methodChannel.setMethodCallHandler(_onMethodCall);
    _imageStream =
        _eventChannel.receiveBroadcastStream().map((data) => data.cast<int>());
  }

  // 添加 start 和 stop 方法
  Future start() async {
    await _methodChannel.invokeMethod('start');
  }

  Future stop() async {
    await _methodChannel.invokeMethod('stop');
  }

  static Stream<List<int>>? get imageStream => _imageStream;
}