删除未使用的相机和记录类来重构记录功能;更新pubspec中的依赖项。Yaml为本地插件路径;简化MainActivity和相关类,以提高性能和可维护性。

This commit is contained in:
2026-07-09 16:17:34 +08:00
parent 75c42ea168
commit 6138eddc16
65 changed files with 3677 additions and 2672 deletions
@@ -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
}
}
}
@@ -0,0 +1,24 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint apivideo_live_stream.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'apivideo_live_stream'
s.version = '0.0.1'
s.summary = 'A new flutter plugin project.'
s.description = <<-DESC
A new flutter plugin project.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.dependency 'ApiVideoLiveStream', "1.4.1"
s.platform = :ios, '12.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
end