删除未使用的相机和记录类来重构记录功能;更新pubspec中的依赖项。Yaml为本地插件路径;简化MainActivity和相关类,以提高性能和可维护性。
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
#import <Flutter/Flutter.h>
|
||||
|
||||
@interface ApiVideoLiveStreamPlugin : NSObject<FlutterPlugin>
|
||||
@end
|
||||
@@ -0,0 +1,15 @@
|
||||
#import "ApiVideoLiveStreamPlugin.h"
|
||||
#if __has_include(<apivideo_live_stream/apivideo_live_stream-Swift.h>)
|
||||
#import <apivideo_live_stream/apivideo_live_stream-Swift.h>
|
||||
#else
|
||||
// Support project import fallback if the generated compatibility header
|
||||
// is not copied when this plugin is created as a library.
|
||||
// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816
|
||||
#import "apivideo_live_stream-Swift.h"
|
||||
#endif
|
||||
|
||||
@implementation ApiVideoLiveStreamPlugin
|
||||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
|
||||
[SwiftApiVideoLiveStreamPlugin registerWithRegistrar:registrar];
|
||||
}
|
||||
@end
|
||||
@@ -0,0 +1,148 @@
|
||||
import ApiVideoLiveStream
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
|
||||
class FlutterLiveStreamView: NSObject {
|
||||
private let previewTexture: PreviewTexture
|
||||
private let liveStream: ApiVideoLiveStream
|
||||
|
||||
private let eventChannel: FlutterEventChannel
|
||||
private var eventSink: FlutterEventSink?
|
||||
|
||||
init(binaryMessenger: FlutterBinaryMessenger, textureRegistry: FlutterTextureRegistry) throws {
|
||||
previewTexture = PreviewTexture(registry: textureRegistry)
|
||||
liveStream = try ApiVideoLiveStream(preview: previewTexture, initialAudioConfig: nil, initialVideoConfig: nil, initialCamera: nil)
|
||||
eventChannel = FlutterEventChannel(name: "video.api.livestream/events", binaryMessenger: binaryMessenger)
|
||||
|
||||
super.init()
|
||||
|
||||
liveStream.delegate = self
|
||||
eventChannel.setStreamHandler(self)
|
||||
}
|
||||
|
||||
var textureId: Int64 {
|
||||
previewTexture.textureId
|
||||
}
|
||||
|
||||
private(set) var isStreaming = false
|
||||
|
||||
var videoConfig: VideoConfig {
|
||||
get {
|
||||
liveStream.videoConfig
|
||||
}
|
||||
set {
|
||||
sendEvent(["type": "videoSizeChanged", "width": Double(newValue.resolution.width), "height": Double(newValue.resolution.height)])
|
||||
|
||||
liveStream.videoConfig = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var audioConfig: AudioConfig {
|
||||
get {
|
||||
liveStream.audioConfig
|
||||
}
|
||||
set {
|
||||
liveStream.audioConfig = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var isMuted: Bool {
|
||||
get {
|
||||
liveStream.isMuted
|
||||
}
|
||||
set {
|
||||
liveStream.isMuted = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var cameraPosition: String {
|
||||
get {
|
||||
if liveStream.cameraPosition == AVCaptureDevice.Position.back {
|
||||
return "back"
|
||||
} else if liveStream.cameraPosition == AVCaptureDevice.Position.front {
|
||||
return "front"
|
||||
} else {
|
||||
return "other"
|
||||
}
|
||||
}
|
||||
set {
|
||||
if newValue == "back" {
|
||||
liveStream.cameraPosition = AVCaptureDevice.Position.back
|
||||
} else if newValue == "front" {
|
||||
liveStream.cameraPosition = AVCaptureDevice.Position.front
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dispose() {
|
||||
liveStream.stopStreaming()
|
||||
liveStream.stopPreview()
|
||||
|
||||
previewTexture.dispose()
|
||||
}
|
||||
|
||||
func startPreview() {
|
||||
liveStream.startPreview()
|
||||
}
|
||||
|
||||
func stopPreview() {
|
||||
liveStream.stopPreview()
|
||||
}
|
||||
|
||||
func startStreaming(streamKey: String, url: String) throws {
|
||||
try liveStream.startStreaming(streamKey: streamKey, url: url)
|
||||
isStreaming = true
|
||||
}
|
||||
|
||||
func stopStreaming() {
|
||||
liveStream.stopStreaming()
|
||||
isStreaming = false
|
||||
}
|
||||
}
|
||||
|
||||
extension FlutterLiveStreamView: FlutterStreamHandler {
|
||||
func onListen(withArguments _: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
|
||||
eventSink = events
|
||||
return nil
|
||||
}
|
||||
|
||||
func onCancel(withArguments _: Any?) -> FlutterError? {
|
||||
eventSink = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
private func sendEvent(_ event: [String: Any]) {
|
||||
DispatchQueue.main.async {
|
||||
self.eventSink?(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FlutterLiveStreamView: ApiVideoLiveStreamDelegate {
|
||||
/// Called when the connection to the rtmp server is successful
|
||||
func connectionSuccess() {
|
||||
sendEvent(["type": "connected"])
|
||||
}
|
||||
|
||||
/// Called when the connection to the rtmp server failed
|
||||
func connectionFailed(_: String) {
|
||||
isStreaming = false
|
||||
sendEvent(["type": "connectionFailed", "message": "Failed to connect"])
|
||||
}
|
||||
|
||||
/// Called when the connection to the rtmp server is closed
|
||||
func disconnection() {
|
||||
isStreaming = false
|
||||
sendEvent(["type": "disconnected"])
|
||||
}
|
||||
|
||||
/// Called if an error happened during the audio configuration
|
||||
func audioError(_ error: Error) {
|
||||
print("audio error: \(error)")
|
||||
}
|
||||
|
||||
/// Called if an error happened during the video configuration
|
||||
func videoError(_ error: Error) {
|
||||
print("video error: \(error)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
import HaishinKit
|
||||
|
||||
class PreviewTexture: NSObject, FlutterTexture {
|
||||
var videoOrientation: AVCaptureVideoOrientation = .portrait
|
||||
var isCaptureVideoPreviewEnabled: Bool = false
|
||||
|
||||
private weak var currentStream: IOStream? {
|
||||
didSet {
|
||||
currentStream?.drawable = self
|
||||
}
|
||||
}
|
||||
|
||||
private var currentSampleBuffer: CMSampleBuffer?
|
||||
private let registry: FlutterTextureRegistry
|
||||
private(set) var textureId: Int64 = 0
|
||||
|
||||
public init(registry: FlutterTextureRegistry) {
|
||||
self.registry = registry
|
||||
super.init()
|
||||
textureId = self.registry.register(self)
|
||||
}
|
||||
|
||||
func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
|
||||
guard let currentSampleBuffer = currentSampleBuffer,
|
||||
let imageBuffer = CMSampleBufferGetImageBuffer(currentSampleBuffer)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Unmanaged<CVPixelBuffer>.passRetained(imageBuffer)
|
||||
}
|
||||
|
||||
func dispose() {
|
||||
registry.unregisterTexture(textureId)
|
||||
}
|
||||
}
|
||||
|
||||
extension PreviewTexture: IOStreamDrawable {
|
||||
// MARK: - IOStreamDrawable
|
||||
func attachStream(_ stream: IOStream?) {
|
||||
if Thread.isMainThread {
|
||||
currentStream = stream
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.currentStream = stream
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func enqueue(_ sampleBuffer: CMSampleBuffer?) {
|
||||
currentSampleBuffer = sampleBuffer
|
||||
registry.textureFrameAvailable(textureId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import ApiVideoLiveStream
|
||||
import AVFoundation
|
||||
import Flutter
|
||||
import HaishinKit
|
||||
import Network
|
||||
import UIKit
|
||||
|
||||
enum ApiVideoLiveStreamError: Error {
|
||||
case invalidAVSession
|
||||
}
|
||||
|
||||
public class SwiftApiVideoLiveStreamPlugin: NSObject, FlutterPlugin {
|
||||
private let binaryMessenger: FlutterBinaryMessenger
|
||||
private let channel: FlutterMethodChannel
|
||||
private let registry: FlutterTextureRegistry
|
||||
private var flutterView: FlutterLiveStreamView?
|
||||
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let instance = SwiftApiVideoLiveStreamPlugin(registrar: registrar)
|
||||
registrar.publish(instance)
|
||||
}
|
||||
|
||||
public init(registrar: FlutterPluginRegistrar) {
|
||||
binaryMessenger = registrar.messenger()
|
||||
channel = FlutterMethodChannel(name: "video.api.livestream/controller", binaryMessenger: binaryMessenger)
|
||||
registry = registrar.textures()
|
||||
super.init()
|
||||
|
||||
registrar.addMethodCallDelegate(self, channel: channel)
|
||||
}
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
switch call.method {
|
||||
case "create":
|
||||
flutterView?.dispose()
|
||||
do {
|
||||
flutterView = try FlutterLiveStreamView(binaryMessenger: binaryMessenger, textureRegistry: registry)
|
||||
if let previewTexture = flutterView?.textureId {
|
||||
result(["textureId": previewTexture])
|
||||
} else {
|
||||
result(FlutterError(code: "failed_to_create_live_stream", message: "Failed to create camera preview surface", details: nil))
|
||||
}
|
||||
} catch {
|
||||
result(FlutterError(code: "failed_to_create_live_stream", message: error.localizedDescription, details: nil))
|
||||
}
|
||||
case "dispose":
|
||||
flutterView?.dispose()
|
||||
case "setVideoConfig":
|
||||
guard let flutterView = flutterView else {
|
||||
result(FlutterError(code: "missing_live_stream", message: "Live stream must exist at this point", details: nil))
|
||||
return
|
||||
}
|
||||
guard let videoParameters = call.arguments as? [String: Any] else {
|
||||
result(FlutterError(code: "invalid_parameter", message: "Invalid video config", details: nil))
|
||||
return
|
||||
}
|
||||
applyVideoConfig(config: videoParameters, flutterView: flutterView, result: result)
|
||||
case "setAudioConfig":
|
||||
guard let flutterView = flutterView else {
|
||||
result(FlutterError(code: "missing_live_stream", message: "Live stream must exist at this point", details: nil))
|
||||
return
|
||||
}
|
||||
guard let audioParameters = call.arguments as? [String: Any] else {
|
||||
result(FlutterError(code: "invalid_parameter", message: "Invalid audio config", details: nil))
|
||||
return
|
||||
}
|
||||
applyAudioConfig(config: audioParameters, flutterView: flutterView, result: result)
|
||||
case "startPreview":
|
||||
guard let flutterView = flutterView else {
|
||||
result(FlutterError(code: "missing_live_stream", message: "Live stream must exist at this point", details: nil))
|
||||
return
|
||||
}
|
||||
flutterView.startPreview()
|
||||
result(nil)
|
||||
case "stopPreview":
|
||||
guard let flutterView = flutterView else {
|
||||
result(FlutterError(code: "missing_live_stream", message: "Live stream must exist at this point", details: nil))
|
||||
return
|
||||
}
|
||||
flutterView.stopPreview()
|
||||
result(nil)
|
||||
case "startStreaming":
|
||||
if let args = call.arguments as? [String: Any] {
|
||||
let streamKey = args["streamKey"] as? String
|
||||
let url = args["url"] as? String
|
||||
if streamKey == nil {
|
||||
result(FlutterError(code: "missing_stream_key", message: "Stream key is missing", details: nil))
|
||||
} else if url == nil {
|
||||
result(FlutterError(code: "missing_rtmp_url", message: "RTMP URL is missing", details: nil))
|
||||
} else {
|
||||
guard let flutterView = flutterView else {
|
||||
result(FlutterError(code: "missing_live_stream", message: "Live stream must exist at this point", details: nil))
|
||||
return
|
||||
}
|
||||
do {
|
||||
try flutterView.startStreaming(streamKey: streamKey!, url: url!)
|
||||
result(nil)
|
||||
} catch {
|
||||
result(FlutterError(code: "missing_live_stream", message: error.localizedDescription, details: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
case "stopStreaming":
|
||||
guard let flutterView = flutterView else {
|
||||
result(FlutterError(code: "missing_live_stream", message: "Live stream must exist at this point", details: nil))
|
||||
return
|
||||
}
|
||||
flutterView.stopStreaming()
|
||||
result(nil)
|
||||
case "getIsStreaming":
|
||||
guard let flutterView = flutterView else {
|
||||
result(FlutterError(code: "missing_live_stream", message: "Live stream must exist at this point", details: nil))
|
||||
return
|
||||
}
|
||||
result(["isStreaming": flutterView.isStreaming])
|
||||
case "getCameraPosition":
|
||||
guard let flutterView = flutterView else {
|
||||
result(FlutterError(code: "missing_live_stream", message: "Live stream must exist at this point", details: nil))
|
||||
return
|
||||
}
|
||||
result(["position": flutterView.cameraPosition])
|
||||
case "setCameraPosition":
|
||||
guard let flutterView = flutterView else {
|
||||
result(FlutterError(code: "missing_live_stream", message: "Live stream must exist at this point", details: nil))
|
||||
return
|
||||
}
|
||||
guard let args = call.arguments as? [String: Any],
|
||||
let cameraPosition = args["position"] as? String
|
||||
else {
|
||||
result(FlutterError(code: "invalid_parameter", message: "Invalid camera position", details: nil))
|
||||
return
|
||||
}
|
||||
flutterView.cameraPosition = cameraPosition
|
||||
result(nil)
|
||||
case "getIsMuted":
|
||||
guard let flutterView = flutterView else {
|
||||
result(FlutterError(code: "missing_live_stream", message: "Live stream must exist at this point", details: nil))
|
||||
return
|
||||
}
|
||||
result(["isMuted": flutterView.isMuted])
|
||||
case "setIsMuted":
|
||||
guard let flutterView = flutterView else {
|
||||
result(FlutterError(code: "missing_live_stream", message: "Live stream must exist at this point", details: nil))
|
||||
return
|
||||
}
|
||||
guard let args = call.arguments as? [String: Any],
|
||||
let isMuted = args["isMuted"] as? Bool
|
||||
else {
|
||||
result(FlutterError(code: "invalid_parameter", message: "Invalid isMuted", details: nil))
|
||||
return
|
||||
}
|
||||
flutterView.isMuted = isMuted
|
||||
result(nil)
|
||||
case "getVideoSize":
|
||||
guard let flutterView = flutterView else {
|
||||
result(FlutterError(code: "missing_live_stream", message: "Live stream must exist at this point", details: nil))
|
||||
return
|
||||
}
|
||||
result(["width": flutterView.videoConfig.resolution.width, "height": flutterView.videoConfig.resolution.height])
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
private func applyVideoConfig(config: Dictionary<String, Any>, flutterView: FlutterLiveStreamView, result: @escaping FlutterResult) {
|
||||
let resolutionString = config["resolution"] as! String?
|
||||
guard let resolutionString else {
|
||||
result(FlutterError(code: "missing_parameter", message: "Resolution is missing", details: nil))
|
||||
return
|
||||
}
|
||||
let resolution = resolutionString.toResolution()
|
||||
guard let resolution else {
|
||||
result(FlutterError(code: "invalid_parameter", message: "Invalid resolution \(resolutionString)", details: nil))
|
||||
return
|
||||
}
|
||||
flutterView.videoConfig = VideoConfig(bitrate: config["bitrate"] as! Int,
|
||||
resolution: resolution.rawValue,
|
||||
fps: config["fps"] as! Float64)
|
||||
result(nil)
|
||||
}
|
||||
|
||||
private func applyAudioConfig(config: Dictionary<String, Any>, flutterView: FlutterLiveStreamView, result: @escaping FlutterResult) {
|
||||
flutterView.audioConfig = AudioConfig(bitrate: config["bitrate"] as! Int)
|
||||
result(nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
func toResolution() -> Resolution? {
|
||||
switch self {
|
||||
case "240p":
|
||||
return Resolution.RESOLUTION_16_9_240P
|
||||
case "360p":
|
||||
return Resolution.RESOLUTION_16_9_360P
|
||||
case "480p":
|
||||
return Resolution.RESOLUTION_16_9_480P
|
||||
case "720p":
|
||||
return Resolution.RESOLUTION_16_9_720P
|
||||
case "1080p":
|
||||
return Resolution.RESOLUTION_16_9_1080P
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user