diff --git a/android/src/main/kotlin/com/haishinkit/haishin_kit/RtmpStreamHandler.kt b/android/src/main/kotlin/com/haishinkit/haishin_kit/RtmpStreamHandler.kt index dc588a3..a852454 100644 --- a/android/src/main/kotlin/com/haishinkit/haishin_kit/RtmpStreamHandler.kt +++ b/android/src/main/kotlin/com/haishinkit/haishin_kit/RtmpStreamHandler.kt @@ -20,10 +20,8 @@ import io.flutter.plugin.common.MethodChannel import java.lang.Exception class RtmpStreamHandler( - private val plugin: HaishinKitPlugin, - handler: RtmpConnectionHandler? -) : MethodChannel.MethodCallHandler, IEventListener, - EventChannel.StreamHandler { + private val plugin: HaishinKitPlugin, handler: RtmpConnectionHandler? +) : MethodChannel.MethodCallHandler, IEventListener, EventChannel.StreamHandler { companion object { private const val TAG = "RtmpStream" } @@ -46,24 +44,54 @@ class RtmpStreamHandler( instance = RtmpStream(it) } channel = EventChannel( - plugin.flutterPluginBinding.binaryMessenger, - "com.haishinkit.eventchannel/${hashCode()}" + plugin.flutterPluginBinding.binaryMessenger, "com.haishinkit.eventchannel/${hashCode()}" ) channel.setStreamHandler(this) } override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { + "$TAG#getHasAudio" -> { + result.success(instance?.audioSetting?.muted) + } + + "$TAG#setHasAudio" -> { + val value = call.argument("value") + value?.let { + instance?.audioSetting?.muted = !it + } + result.success(null) + } + + "$TAG#getHasVideo" -> { + result.success(null) + } + + "$TAG#setHasVideo" -> { + result.success(null) + } + + "$TAG#setFrameRate" -> { + val value = call.argument("value") + value?.let { + instance?.videoSetting?.frameRate = it + } + result.success(null) + } + + "$TAG#setSessionPreset" -> { + // for iOS + result.success(null) + } + "$TAG#setAudioSettings" -> { val source = call.argument>("settings") ?: return (source["bitrate"] as? Int)?.let { instance?.audioSetting?.bitRate = it } - (source["muted"] as? Boolean)?.let { - instance?.audioSetting?.muted = it - } result.success(null) } + "$TAG#setVideoSettings" -> { val source = call.argument>("settings") ?: return (source["width"] as? Int)?.let { @@ -91,9 +119,7 @@ class RtmpStreamHandler( } result.success(null) } - "$TAG#setCaptureSettings" -> { - result.success(null) - } + "$TAG#attachAudio" -> { val source = call.argument>("source") if (source == null) { @@ -103,6 +129,7 @@ class RtmpStreamHandler( } result.success(null) } + "$TAG#attachVideo" -> { val source = call.argument>("source") if (source == null) { @@ -114,6 +141,7 @@ class RtmpStreamHandler( "front" -> { facing = CameraCharacteristics.LENS_FACING_FRONT } + "back" -> { facing = CameraCharacteristics.LENS_FACING_BACK } @@ -131,6 +159,7 @@ class RtmpStreamHandler( } result.success(null) } + "$TAG#registerTexture" -> { val netStream = instance if (netStream?.drawable == null) { @@ -144,18 +173,19 @@ class RtmpStreamHandler( val texture = (netStream.drawable as? NetStreamDrawableTexture) val width = call.argument("width") ?: 0 val height = call.argument("height") ?: 0 - texture?.imageExtent = - Size(width.toInt(), height.toInt()) + texture?.imageExtent = Size(width.toInt(), height.toInt()) (plugin.flutterPluginBinding.applicationContext.getSystemService(Context.WINDOW_SERVICE) as? WindowManager)?.defaultDisplay?.orientation?.let { netStream.deviceOrientation = it } result.success(texture?.id) } } + "$TAG#publish" -> { instance?.publish(call.argument("name")) result.success(null) } + "$TAG#play" -> { val name = call.argument("name") if (name != null) { @@ -163,9 +193,12 @@ class RtmpStreamHandler( } result.success(null) } + "$TAG#close" -> { instance?.close() + result.success(null) } + "$TAG#dispose" -> { eventSink?.endOfStream() instance?.close() diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 9cfae4f..ce0df14 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2,13 +2,13 @@ PODS: - audio_session (0.0.1): - Flutter - Flutter (1.0.0) - - haishin_kit (0.9.2): + - haishin_kit (0.10.1): - Flutter - - HaishinKit (= 1.3.0) - - HaishinKit (1.3.0): - - Logboard (~> 2.3.0) - - Logboard (2.3.0) - - permission_handler_apple (9.0.4): + - HaishinKit (= 1.5.2) + - HaishinKit (1.5.2): + - Logboard (~> 2.3.1) + - Logboard (2.3.1) + - permission_handler_apple (9.1.0): - Flutter DEPENDENCIES: @@ -35,11 +35,11 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: audio_session: 4f3e461722055d21515cf3261b64c973c062f345 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - haishin_kit: 2e92b4d42bccda32bf35fa86a105fd7c5746dade - HaishinKit: 013229683accc4abe46630b04629da3ff90428b7 - Logboard: f3a37d2a4040b82b7e2eb39765897e2c9c5ec024 - permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce + haishin_kit: a7ed49a4b47b032202c8cca4a80544d4513f690d + HaishinKit: b42a0e86766957a1882c0f61f0ebf0111d581402 + Logboard: 3d98bb85de6a36b255ab637e8178eb5671c5c3a6 + permission_handler_apple: 8f116445eff3c0e7c65ad60f5fef5490aa94b4e4 PODFILE CHECKSUM: 1d4f584ecee31f81e6b1ae37103ab21a5f7aac05 -COCOAPODS: 1.11.3 +COCOAPODS: 1.12.1 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index c8bcb6a..ade1a77 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -338,10 +338,12 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -352,6 +354,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index bdc2e72..1ad7771 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -57,5 +57,7 @@ UIViewControllerBasedStatusBarAppearance + UIApplicationSupportsIndirectInputEvents + diff --git a/example/lib/main.dart b/example/lib/main.dart index 1b16037..62f3c76 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -71,7 +71,7 @@ class _MyAppState extends State { }); RtmpStream stream = await RtmpStream.create(connection); - stream.audioSettings = AudioSettings(muted: false, bitrate: 64 * 1000); + stream.audioSettings = AudioSettings(bitrate: 64 * 1000); stream.videoSettings = VideoSettings( width: 480, height: 272, diff --git a/example/pubspec.lock b/example/pubspec.lock index 6e4ca81..0f95b1a 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: "direct main" description: name: audio_session - sha256: "655343841a723646f74932215c5785ef7156b76d2de4b99bcd3205476f08dc61" + sha256: "8a2bc5e30520e18f3fb0e366793d78057fb64cd5287862c76af0c8771f2a52ad" url: "https://pub.dev" source: hosted - version: "0.1.15" + version: "0.1.16" boolean_selector: dependency: transitive description: @@ -94,7 +94,7 @@ packages: path: ".." relative: true source: path - version: "0.10.0" + version: "0.11.0" js: dependency: transitive description: @@ -147,34 +147,34 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "33c6a1253d1f95fd06fa74b65b7ba907ae9811f9d5c1d3150e51417d04b8d6a8" + sha256: "1b6b3e73f0bcbc856548bbdfb1c33084a401c4f143e220629a9055233d76c331" url: "https://pub.dev" source: hosted - version: "10.2.0" + version: "10.3.0" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: d8cc6a62ded6d0f49c6eac337e080b066ee3bce4d405bd9439a61e1f1927bfe8 + sha256: "8f6a95ccbca13766882f95d32684d7c9bfe6c45650c32bedba948ef1c6a4ddf7" url: "https://pub.dev" source: hosted - version: "10.2.1" + version: "10.2.3" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: ee96ac32f5a8e6f80756e25b25b9f8e535816c8e6665a96b6d70681f8c4f7e85 + sha256: "08dcb6ce628ac0b257e429944b4c652c2a4e6af725bdf12b498daa2c6b2b1edb" url: "https://pub.dev" source: hosted - version: "9.0.8" + version: "9.1.0" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: "68abbc472002b5e6dfce47fe9898c6b7d8328d58b5d2524f75e277c07a97eb84" + sha256: de20a5c3269229c1ae2e5a6b822f6cb59578b23e8255c93fbeebfc82116e6b11 url: "https://pub.dev" source: hosted - version: "3.9.0" + version: "3.10.0" permission_handler_windows: dependency: transitive description: diff --git a/ios/Classes/ASObjectUil.swift b/ios/Classes/ASObjectUil.swift index 55884d4..aea116b 100644 --- a/ios/Classes/ASObjectUil.swift +++ b/ios/Classes/ASObjectUil.swift @@ -4,7 +4,7 @@ import HaishinKit enum ASObjectUtil { static func removeEmpty(_ value: Any?) -> Any? { if var value = value as? ASObject { - for var element in value { + for element in value { value[element.key] = removeEmpty(element.value) } return value.isEmpty ? nil : value @@ -17,7 +17,7 @@ enum ASObjectUtil { return result.isEmpty ? nil : result } if let value = value { - if (value is ASUndefined) { + if value is ASUndefined { return nil } return value @@ -25,4 +25,3 @@ enum ASObjectUtil { return nil } } - diff --git a/ios/Classes/NetStreamDrawableTexture.swift b/ios/Classes/NetStreamDrawableTexture.swift index 729f754..d6414bc 100644 --- a/ios/Classes/NetStreamDrawableTexture.swift +++ b/ios/Classes/NetStreamDrawableTexture.swift @@ -7,7 +7,7 @@ class NetStreamDrawableTexture: NSObject, FlutterTexture { static let defaultOptions: [String: Any] = [ kCVPixelBufferCGImageCompatibilityKey as String: true, kCVPixelBufferCGBitmapContextCompatibilityKey as String: true, - kCVPixelBufferIOSurfacePropertiesKey as String: [:] + kCVPixelBufferIOSurfacePropertiesKey as String: NSDictionary() ] var id: Int64 = 0 @@ -16,6 +16,7 @@ class NetStreamDrawableTexture: NSObject, FlutterTexture { var videoFormatDescription: CMVideoFormatDescription? var bounds: CGSize = .zero var videoGravity: AVLayerVideoGravity = .resizeAspectFill + var videoOrientation: AVCaptureVideoOrientation = .portrait private var currentSampleBuffer: CMSampleBuffer? private let registry: FlutterTextureRegistry private let context = CIContext() diff --git a/ios/Classes/ProfileLevel.swift b/ios/Classes/ProfileLevel.swift index bbfe050..3802531 100644 --- a/ios/Classes/ProfileLevel.swift +++ b/ios/Classes/ProfileLevel.swift @@ -3,7 +3,6 @@ import AVFoundation import HaishinKit import VideoToolbox - enum ProfileLevel: String { case H264_Baseline_1_3 = "H264_Baseline_1_3" case H264_Baseline_3_0 = "H264_Baseline_3_0" @@ -41,86 +40,86 @@ enum ProfileLevel: String { case H264_Main_5_2 = "H264_Main_5_2" case H264_Main_AutoLevel = "H264_Main_AutoLevel" - var kVTProfileLevel: CFString { + var kVTProfileLevel: String { switch self { case .H264_Baseline_1_3: - return kVTProfileLevel_H264_Baseline_1_3 + return kVTProfileLevel_H264_Baseline_1_3 as String case .H264_Baseline_3_0: - return kVTProfileLevel_H264_Baseline_3_0 + return kVTProfileLevel_H264_Baseline_3_0 as String case .H264_Baseline_3_1: - return kVTProfileLevel_H264_Baseline_3_1 + return kVTProfileLevel_H264_Baseline_3_1 as String case .H264_Baseline_3_2: - return kVTProfileLevel_H264_Baseline_3_2 + return kVTProfileLevel_H264_Baseline_3_2 as String case .H264_Baseline_4_0: - return kVTProfileLevel_H264_Baseline_4_0 + return kVTProfileLevel_H264_Baseline_4_0 as String case .H264_Baseline_4_1: - return kVTProfileLevel_H264_Baseline_4_1 + return kVTProfileLevel_H264_Baseline_4_1 as String case .H264_Baseline_4_2: - return kVTProfileLevel_H264_Baseline_4_2 + return kVTProfileLevel_H264_Baseline_4_2 as String case .H264_Baseline_5_0: - return kVTProfileLevel_H264_Baseline_5_0 + return kVTProfileLevel_H264_Baseline_5_0 as String case .H264_Baseline_5_1: - return kVTProfileLevel_H264_Baseline_5_1 + return kVTProfileLevel_H264_Baseline_5_1 as String case .H264_Baseline_5_2: - return kVTProfileLevel_H264_Baseline_5_2 + return kVTProfileLevel_H264_Baseline_5_2 as String case .H264_Baseline_AutoLevel: - return kVTProfileLevel_H264_Baseline_AutoLevel + return kVTProfileLevel_H264_Baseline_AutoLevel as String case .H264_ConstrainedBaseline_AutoLevel: if #available(iOS 15.0, *) { - return kVTProfileLevel_H264_ConstrainedBaseline_AutoLevel + return kVTProfileLevel_H264_ConstrainedBaseline_AutoLevel as String } else { - return kVTProfileLevel_H264_Baseline_AutoLevel + return kVTProfileLevel_H264_Baseline_AutoLevel as String } case .H264_ConstrainedHigh_AutoLevel: if #available(iOS 15.0, *) { - return kVTProfileLevel_H264_ConstrainedHigh_AutoLevel + return kVTProfileLevel_H264_ConstrainedHigh_AutoLevel as String } else { - return kVTProfileLevel_H264_High_AutoLevel + return kVTProfileLevel_H264_High_AutoLevel as String } case .H264_Extended_5_0: - return kVTProfileLevel_H264_Extended_5_0 + return kVTProfileLevel_H264_Extended_5_0 as String case .H264_Extended_AutoLevel: - return kVTProfileLevel_H264_Extended_AutoLevel + return kVTProfileLevel_H264_Extended_AutoLevel as String case .H264_High_3_0: - return kVTProfileLevel_H264_High_3_0 + return kVTProfileLevel_H264_High_3_0 as String case .H264_High_3_1: - return kVTProfileLevel_H264_High_3_1 + return kVTProfileLevel_H264_High_3_1 as String case .H264_High_3_2: - return kVTProfileLevel_H264_High_3_2 + return kVTProfileLevel_H264_High_3_2 as String case .H264_High_4_0: - return kVTProfileLevel_H264_High_4_0 + return kVTProfileLevel_H264_High_4_0 as String case .H264_High_4_1: - return kVTProfileLevel_H264_High_4_1 + return kVTProfileLevel_H264_High_4_1 as String case .H264_High_4_2: - return kVTProfileLevel_H264_High_4_2 + return kVTProfileLevel_H264_High_4_2 as String case .H264_High_5_0: - return kVTProfileLevel_H264_High_5_0 + return kVTProfileLevel_H264_High_5_0 as String case .H264_High_5_1: - return kVTProfileLevel_H264_High_5_1 + return kVTProfileLevel_H264_High_5_1 as String case .H264_High_5_2: - return kVTProfileLevel_H264_High_5_2 + return kVTProfileLevel_H264_High_5_2 as String case .H264_High_AutoLevel: - return kVTProfileLevel_H264_High_AutoLevel + return kVTProfileLevel_H264_High_AutoLevel as String case .H264_Main_3_0: - return kVTProfileLevel_H264_Main_3_0 + return kVTProfileLevel_H264_Main_3_0 as String case .H264_Main_3_1: - return kVTProfileLevel_H264_Main_3_1 + return kVTProfileLevel_H264_Main_3_1 as String case .H264_Main_3_2: - return kVTProfileLevel_H264_Main_3_2 + return kVTProfileLevel_H264_Main_3_2 as String case .H264_Main_4_0: - return kVTProfileLevel_H264_Main_4_0 + return kVTProfileLevel_H264_Main_4_0 as String case .H264_Main_4_1: - return kVTProfileLevel_H264_Main_4_1 + return kVTProfileLevel_H264_Main_4_1 as String case .H264_Main_4_2: - return kVTProfileLevel_H264_Main_4_2 + return kVTProfileLevel_H264_Main_4_2 as String case .H264_Main_5_0: - return kVTProfileLevel_H264_Main_5_0 + return kVTProfileLevel_H264_Main_5_0 as String case .H264_Main_5_1: - return kVTProfileLevel_H264_Main_5_1 + return kVTProfileLevel_H264_Main_5_1 as String case .H264_Main_5_2: - return kVTProfileLevel_H264_Main_5_2 + return kVTProfileLevel_H264_Main_5_2 as String case .H264_Main_AutoLevel: - return kVTProfileLevel_H264_Main_AutoLevel + return kVTProfileLevel_H264_Main_AutoLevel as String } } } diff --git a/ios/Classes/RTMPStreamHandler.swift b/ios/Classes/RTMPStreamHandler.swift index 9310c86..872de09 100644 --- a/ios/Classes/RTMPStreamHandler.swift +++ b/ios/Classes/RTMPStreamHandler.swift @@ -24,7 +24,7 @@ class RTMPStreamHandler: NSObject, MethodCallHandler { let instance = RTMPStream(connection: connection) instance.addEventListener(.rtmpStatus, selector: #selector(RTMPStreamHandler.handler), observer: self) if let orientation = DeviceUtil.videoOrientation(by: UIApplication.shared.statusBarOrientation) { - instance.orientation = orientation + instance.videoOrientation = orientation } NotificationCenter.default.addObserver(self, selector: #selector(on(_:)), name: UIDevice.orientationDidChangeNotification, object: nil) self.instance = instance @@ -34,104 +34,93 @@ class RTMPStreamHandler: NSObject, MethodCallHandler { func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let arguments = call.arguments as? [String: Any?] else { + result(nil) return } switch call.method { + case "RtmpStream#getHasAudio": + result(instance?.hasAudio) + case "RtmpStream#setHasAudio": + guard let hasAudio = arguments["value"] as? Bool else { + result(nil) + return + } + instance?.hasAudio = hasAudio + result(nil) + case "RtmpStream#getHasVudio": + result(instance?.hasVideo) + case "RtmpStream#setHasVudio": + guard let hasVideo = arguments["value"] as? Bool else { + result(nil) + return + } + instance?.hasVideo = hasVideo + result(nil) + case "RtmpStream#setFrameRate": + guard + let frameRate = arguments["value"] as? NSNumber else { + result(nil) + return + } + instance?.frameRate = frameRate.doubleValue + result(nil) + case "RtmpStream#setSessionPreset": + guard let sessionPreset = arguments["value"] as? String else { + result(nil) + return + } + switch sessionPreset { + case "high": + instance?.sessionPreset = .high + case "medium": + instance?.sessionPreset = .medium + case "low": + instance?.sessionPreset = .low + case "hd1280x720": + instance?.sessionPreset = .hd1280x720 + case "hd1920x1080": + instance?.sessionPreset = .hd1920x1080 + case "hd4K3840x2160": + instance?.sessionPreset = .hd4K3840x2160 + case "vga640x480": + instance?.sessionPreset = .vga640x480 + case "iFrame960x540": + instance?.sessionPreset = .iFrame960x540 + case "iFrame1280x720": + instance?.sessionPreset = .iFrame1280x720 + case "cif352x288": + instance?.sessionPreset = .cif352x288 + default: + instance?.sessionPreset = AVCaptureSession.Preset.hd1280x720 + } + result(nil) case "RtmpStream#setAudioSettings": guard let settings = arguments["settings"] as? [String: Any?] else { + result(nil) return } - if let muted = settings["muted"] as? Bool { - instance?.audioSettings[.muted] = muted - } if let bitrate = settings["bitrate"] as? NSNumber { - instance?.audioSettings[.bitrate] = bitrate.intValue + instance?.audioSettings.bitRate = bitrate.intValue } result(nil) case "RtmpStream#setVideoSettings": guard let settings = arguments["settings"] as? [String: Any?] else { + result(nil) return } - if let muted = settings["muted"] as? Bool { - instance?.videoSettings[.muted] = muted - } if let bitrate = settings["bitrate"] as? NSNumber { - instance?.videoSettings[.bitrate] = bitrate.intValue + instance?.videoSettings.bitRate = bitrate.uint32Value } - if let width = settings["width"] as? NSNumber { - instance?.videoSettings[.width] = width.intValue - } - if let height = settings["height"] as? NSNumber { - instance?.videoSettings[.height] = height.intValue + if let width = settings["width"] as? NSNumber, let height = settings["height"] as? NSNumber { + instance?.videoSettings.videoSize = .init(width: width.int32Value, height: height.int32Value) } if let frameInterval = settings["frameInterval"] as? NSNumber { - instance?.videoSettings[.maxKeyFrameIntervalDuration] = frameInterval.intValue + instance?.videoSettings.maxKeyFrameIntervalDuration = frameInterval.int32Value } if let profileLevel = settings["profileLevel"] as? String { - instance?.videoSettings[.profileLevel] = ProfileLevel(rawValue: profileLevel)?.kVTProfileLevel ?? ProfileLevel.H264_Baseline_AutoLevel - } - result(nil) - case "RtmpStream#setCaptureSettings": - guard - let settings = arguments["settings"] as? [String: Any?] else { - return - } - if let fps = settings["fps"] as? NSNumber { - instance?.captureSettings[.fps] = fps.intValue - } - if let continuousAutofocus = settings["continuousAutofocus"] as? Bool { - instance?.captureSettings[.continuousAutofocus] = continuousAutofocus - } - if let continuousExposure = settings["continuousExposure"] as? Bool { - instance?.captureSettings[.continuousExposure] = continuousExposure - } - if let sessionPreset = settings["sessionPreset"] as? String { - switch sessionPreset { - case "high": - instance?.videoSettings[.profileLevel] = kVTProfileLevel_H264_High_AutoLevel - instance?.captureSettings[.sessionPreset] = AVCaptureSession.Preset.high - case "medium": - instance?.videoSettings[.profileLevel] = kVTProfileLevel_H264_Main_AutoLevel - instance?.captureSettings[.sessionPreset] = AVCaptureSession.Preset.medium - case "low": - instance?.videoSettings[.profileLevel] = kVTProfileLevel_H264_Baseline_3_1 - instance?.captureSettings[.sessionPreset] = AVCaptureSession.Preset.low - case "photo": - instance?.videoSettings[.profileLevel] = kVTProfileLevel_H264_High_AutoLevel - instance?.captureSettings[.sessionPreset] = AVCaptureSession.Preset.photo - case "qHD960x540": - // macos only - () - case "hd1280x720": - instance?.videoSettings[.profileLevel] = kVTProfileLevel_H264_High_AutoLevel - instance?.captureSettings[.sessionPreset] = AVCaptureSession.Preset.hd1280x720 - case "hd1920x1080": - instance?.videoSettings[.profileLevel] = kVTProfileLevel_H264_High_AutoLevel - instance?.captureSettings[.sessionPreset] = AVCaptureSession.Preset.hd1920x1080 - case "hd4K3840x2160": - instance?.videoSettings[.profileLevel] = kVTProfileLevel_H264_High_AutoLevel - instance?.captureSettings[.sessionPreset] = AVCaptureSession.Preset.hd4K3840x2160 - case "qvga320x240": - // macos only - () - case "vga640x480": - instance?.videoSettings[.profileLevel] = kVTProfileLevel_H264_High_AutoLevel - instance?.captureSettings[.sessionPreset] = AVCaptureSession.Preset.vga640x480 - case "iFrame960x540": - instance?.videoSettings[.profileLevel] = kVTProfileLevel_H264_High_AutoLevel - instance?.captureSettings[.sessionPreset] = AVCaptureSession.Preset.iFrame960x540 - case "iFrame1280x720": - instance?.videoSettings[.profileLevel] = kVTProfileLevel_H264_High_AutoLevel - instance?.captureSettings[.sessionPreset] = AVCaptureSession.Preset.iFrame1280x720 - case "cif352x288": - instance?.videoSettings[.profileLevel] = kVTProfileLevel_H264_High_AutoLevel - instance?.captureSettings[.sessionPreset] = AVCaptureSession.Preset.cif352x288 - default: - instance?.videoSettings[.profileLevel] = kVTProfileLevel_H264_Main_AutoLevel - instance?.captureSettings[.sessionPreset] = AVCaptureSession.Preset.medium - } + instance?.videoSettings.profileLevel = ProfileLevel(rawValue: profileLevel)?.kVTProfileLevel ?? ProfileLevel.H264_Baseline_AutoLevel.kVTProfileLevel } result(nil) case "RtmpStream#attachAudio": @@ -158,7 +147,9 @@ class RTMPStreamHandler: NSObject, MethodCallHandler { break } } - instance?.attachCamera(DeviceUtil.device(withPosition: devicePosition)) + if let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: devicePosition) { + instance?.attachCamera(device) + } } result(nil) case "RtmpStream#play": @@ -170,6 +161,7 @@ class RTMPStreamHandler: NSObject, MethodCallHandler { case "RtmpStream#registerTexture": guard let registry = plugin.registrar?.textures() else { + result(nil) return } if instance?.mixer.drawable == nil { @@ -189,6 +181,7 @@ class RTMPStreamHandler: NSObject, MethodCallHandler { } case "RtmpStream#close": instance?.close() + result(nil) case "RtmpStream#dispose": instance?.removeEventListener(.rtmpStatus, selector: #selector(handler)) instance?.close() @@ -214,7 +207,7 @@ class RTMPStreamHandler: NSObject, MethodCallHandler { guard let orientation = DeviceUtil.videoOrientation(by: UIApplication.shared.statusBarOrientation) else { return } - instance?.orientation = orientation + instance?.videoOrientation = orientation } } diff --git a/ios/haishin_kit.podspec b/ios/haishin_kit.podspec index b9f4820..ce69eac 100644 --- a/ios/haishin_kit.podspec +++ b/ios/haishin_kit.podspec @@ -13,7 +13,7 @@ Pod::Spec.new do |s| s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.dependency 'HaishinKit', '1.3.0' + s.dependency 'HaishinKit', '1.5.2' s.platform = :ios, '11.0' # Flutter.framework does not contain a i386 slice. diff --git a/lib/audio_settings.dart b/lib/audio_settings.dart index 623eb3d..bbf7c4e 100644 --- a/lib/audio_settings.dart +++ b/lib/audio_settings.dart @@ -1,11 +1,9 @@ class AudioSettings { - bool muted; int bitrate; // AudioSettings({ - this.muted = false, this.bitrate = 80 * 1000, }); @@ -14,15 +12,14 @@ class AudioSettings { identical(this, other) || (other is AudioSettings && runtimeType == other.runtimeType && - muted == other.muted && bitrate == other.bitrate); @override - int get hashCode => muted.hashCode ^ bitrate.hashCode; + int get hashCode => bitrate.hashCode; @override String toString() { - return 'AudioSettings{' + ' muted: $muted,' + ' bitrate: $bitrate,' + '}'; + return 'AudioSettings{ bitrate: $bitrate,}'; } AudioSettings copyWith({ @@ -30,21 +27,18 @@ class AudioSettings { int? bitrate, }) { return AudioSettings( - muted: muted ?? this.muted, bitrate: bitrate ?? this.bitrate, ); } Map toMap() { return { - 'muted': this.muted, - 'bitrate': this.bitrate, + 'bitrate': bitrate, }; } factory AudioSettings.fromMap(Map map) { return AudioSettings( - muted: map['muted'] as bool, bitrate: map['bitrate'] as int, ); } diff --git a/lib/av_capture_session_preset.dart b/lib/av_capture_session_preset.dart new file mode 100644 index 0000000..38a173b --- /dev/null +++ b/lib/av_capture_session_preset.dart @@ -0,0 +1,17 @@ +enum AVCaptureSessionPreset { + high('high'), + medium('medium'), + low('low'), + qHD960x540('qHD960x540'), + hd1280x720('hd1280x720'), + hd1920x1080('hd1920x1080'), + hd4K3840x2160('hd4K3840x2160'), + vga640x480('vga640x480'), + iFrame960x540('iFrame960x540'), + iFrame1280x720('iFrame1280x720'), + cif352x288('cif352x288'), + ; + + const AVCaptureSessionPreset(this.presetName); + final String presetName; +} diff --git a/lib/capture_settings.dart b/lib/capture_settings.dart deleted file mode 100644 index f1749ca..0000000 --- a/lib/capture_settings.dart +++ /dev/null @@ -1,92 +0,0 @@ -enum AVCaptureSessionPreset { - high('high'), - medium('medium'), - low('low'), - photo('photo'), - qHD960x540('qHD960x540'), - hd1280x720('hd1280x720'), - hd1920x1080('hd1920x1080'), - hd4K3840x2160('hd4K3840x2160'), - qvga320x240('qvga320x240'), - vga640x480('vga640x480'), - iFrame960x540('iFrame960x540'), - iFrame1280x720('iFrame1280x720'), - cif352x288('cif352x288'), - ; - - const AVCaptureSessionPreset(this.presetName); - final String presetName; -} - -class CaptureSettings { - int fps; - bool continuousAutofocus; - bool continuousExposure; - AVCaptureSessionPreset sessionPreset; - - CaptureSettings({ - this.continuousAutofocus = false, - this.continuousExposure = false, - this.fps = 30, - this.sessionPreset = AVCaptureSessionPreset.medium, - }); - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is CaptureSettings && - runtimeType == other.runtimeType && - continuousAutofocus == other.continuousAutofocus && - continuousExposure == other.continuousExposure && - fps == other.fps && - sessionPreset == other.sessionPreset); - - @override - int get hashCode => - fps.hashCode ^ - continuousAutofocus.hashCode ^ - continuousExposure.hashCode ^ - sessionPreset.hashCode; - - @override - String toString() { - return 'CaptureSettings{' + - ' fps: $fps,' + - ' continuousAutofocus: $continuousAutofocus,' + - ' continuousExposure: $continuousExposure,' + - ' sessionPreset: ${sessionPreset.presetName},' + - '}'; - } - - CaptureSettings copyWith({ - int? fps, - bool? continuousAutofocus, - bool? continuousExposure, - AVCaptureSessionPreset? sessionPreset, - }) { - return CaptureSettings( - fps: fps ?? this.fps, - continuousAutofocus: continuousAutofocus ?? this.continuousAutofocus, - continuousExposure: continuousExposure ?? this.continuousExposure, - sessionPreset: sessionPreset ?? this.sessionPreset, - ); - } - - Map toMap() { - return { - 'fps': this.fps, - 'continuousAutofocus': this.continuousAutofocus, - 'continuousExposure': this.continuousExposure, - 'sessionPreset': this.sessionPreset.presetName, - }; - } - - factory CaptureSettings.fromMap(Map map) { - return CaptureSettings( - fps: map['fps'] as int, - continuousAutofocus: map['continuousAutofocus'] as bool, - continuousExposure: map['continuousExposure'] as bool, - sessionPreset: AVCaptureSessionPreset.values.byName(map['sessionPreset'] as String), - ); - } -} diff --git a/lib/net_stream.dart b/lib/net_stream.dart index 711b55e..1498519 100644 --- a/lib/net_stream.dart +++ b/lib/net_stream.dart @@ -1,23 +1,41 @@ import 'package:haishin_kit/audio_source.dart'; +import 'package:haishin_kit/av_capture_session_preset.dart'; import 'package:haishin_kit/video_settings.dart'; import 'package:haishin_kit/video_source.dart'; import 'audio_settings.dart'; -import 'capture_settings.dart'; /// The NetStream class is the foundation of a RTMPStream. abstract class NetStream { /// The memory address. int? get memory; - /// Specifies stream video compression properties. + /// Gets the frameRate. + int get frameRate; + + /// Sets the frameRate. + void set frameRate(int value); + + /// Specifies the sessionPreset for iOS. + set sessionPreset(AVCaptureSessionPreset value); + + /// Specifies the video compression properties. set videoSettings(VideoSettings videoSettings); - /// Specifies stream audio compression properties. + /// Specifies the audio compression properties. set audioSettings(AudioSettings audioSettings); - /// Specifies stream AVSession properties. - set captureSettings(CaptureSettings captureSettings); + /// Gets the hasAuio property. + Future getHasAudio(); + + /// Sets the hasAudio property. + Future setHasAudio(bool value); + + /// Gets the hasVideo property. + Future getHasVideo(); + + /// Sets the hasVideo property. + Future setHasVideo(bool value); /// Attaches an AudioSource to this stream. Future attachAudio(AudioSource? audio); diff --git a/lib/net_stream_drawable_texture.dart b/lib/net_stream_drawable_texture.dart index 5a976b4..63872d4 100644 --- a/lib/net_stream_drawable_texture.dart +++ b/lib/net_stream_drawable_texture.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:haishin_kit/net_stream.dart'; diff --git a/lib/rtmp_stream.dart b/lib/rtmp_stream.dart index f13344f..d85d015 100644 --- a/lib/rtmp_stream.dart +++ b/lib/rtmp_stream.dart @@ -1,5 +1,6 @@ import 'package:flutter/services.dart'; import 'package:haishin_kit/audio_source.dart'; +import 'package:haishin_kit/av_capture_session_preset.dart'; import 'package:haishin_kit/haishin_kit_platform_interface.dart'; import 'package:haishin_kit/net_stream.dart'; import 'package:haishin_kit/rtmp_connection.dart'; @@ -8,7 +9,6 @@ import 'package:haishin_kit/video_settings.dart'; import 'package:haishin_kit/video_source.dart'; import 'audio_settings.dart'; -import 'capture_settings.dart'; class RtmpStream extends NetStream { static Future create(RtmpConnection connection) async { @@ -23,9 +23,10 @@ class RtmpStream extends NetStream { int? _memory; late EventChannel _eventChannel; + int _frameRate = 30; + AVCaptureSessionPreset _sessionPreset = AVCaptureSessionPreset.hd1280x720; VideoSettings _videoSettings = VideoSettings(); AudioSettings _audioSettings = AudioSettings(); - CaptureSettings _captureSettings = CaptureSettings(); RtmpStream._(); @@ -34,6 +35,27 @@ class RtmpStream extends NetStream { EventChannel get eventChannel => _eventChannel; + @override + int get frameRate => _frameRate; + + @override + set frameRate(int frameRate) { + assert(_memory != null); + _frameRate = frameRate; + RtmpStreamPlatform.instance + .setFrameRate({"memory": _memory, "value": frameRate}); + } + + AVCaptureSessionPreset get sessionPreset => _sessionPreset; + + @override + set sessionPreset(AVCaptureSessionPreset sessionPreset) { + assert(_memory != null); + _sessionPreset = sessionPreset; + RtmpStreamPlatform.instance.setSessionPreset( + {"memory": _memory, "value": sessionPreset.presetName}); + } + VideoSettings get videoSettings => _videoSettings; @override @@ -54,14 +76,30 @@ class RtmpStream extends NetStream { {"memory": _memory, "settings": audioSettings.toMap()}); } - CaptureSettings get captureSettings => _captureSettings; + @override + Future setHasAudio(bool value) async { + assert(_memory != null); + RtmpStreamPlatform.instance + .setHasAudio({"memory": _memory, "value": value}); + } + + @override + Future getHasAudio() { + assert(_memory != null); + return RtmpStreamPlatform.instance.getHasAudio(); + } + + @override + Future setHasVideo(bool value) async { + assert(_memory != null); + RtmpStreamPlatform.instance + .setHasVideo({"memory": _memory, "value": value}); + } @override - set captureSettings(CaptureSettings captureSettings) { + Future getHasVideo() { assert(_memory != null); - _captureSettings = captureSettings; - RtmpStreamPlatform.instance.setCaptureSettings( - {"memory": _memory, "settings": captureSettings.toMap()}); + return RtmpStreamPlatform.instance.getHasVideo(); } @override diff --git a/lib/rtmp_stream_method_channel.dart b/lib/rtmp_stream_method_channel.dart index f6338d1..5b0becf 100644 --- a/lib/rtmp_stream_method_channel.dart +++ b/lib/rtmp_stream_method_channel.dart @@ -1,3 +1,4 @@ +import 'package:haishin_kit/av_capture_session_preset.dart'; import 'package:haishin_kit/rtmp_stream_platform_interface.dart'; import 'haishin_kit_method_channel.dart'; @@ -5,21 +6,51 @@ import 'haishin_kit_method_channel.dart'; /// The method channel implementation of [RtmpStreamPlatform] class MethodChannelRtmpStream extends RtmpStreamPlatform { @override - Future setAudioSettings(Map params) async { + Future getHasAudio() async { return await MethodChannelHaishinKit.channel - .invokeMethod("RtmpStream#setAudioSettings", params); + .invokeMethod("RtmpStream#getHasAudio"); } @override - Future setVideoSettings(Map params) async { + Future setHasAudio(Map params) async { return await MethodChannelHaishinKit.channel - .invokeMethod("RtmpStream#setVideoSettings", params); + .invokeMethod("RtmpStream#setHasAudio", params); + } + + @override + Future getHasVideo() async { + return await MethodChannelHaishinKit.channel + .invokeMethod("RtmpStream#getHasVideo"); + } + + @override + Future setHasVideo(Map params) async { + return await MethodChannelHaishinKit.channel + .invokeMethod("RtmpStream#setHasVideo", params); + } + + @override + Future setFrameRate(Map params) async { + return await MethodChannelHaishinKit.channel + .invokeMethod("RtmpStream#setFrameRate", params); + } + + @override + Future setSessionPreset(Map params) async { + return await MethodChannelHaishinKit.channel + .invokeMethod("RtmpStream#setSessionPreset", params); + } + + @override + Future setAudioSettings(Map params) async { + return await MethodChannelHaishinKit.channel + .invokeMethod("RtmpStream#setAudioSettings", params); } @override - Future setCaptureSettings(Map params) async { + Future setVideoSettings(Map params) async { return await MethodChannelHaishinKit.channel - .invokeMethod("RtmpStream#setCaptureSettings", params); + .invokeMethod("RtmpStream#setVideoSettings", params); } @override diff --git a/lib/rtmp_stream_platform_interface.dart b/lib/rtmp_stream_platform_interface.dart index 09b89f8..285f0fb 100644 --- a/lib/rtmp_stream_platform_interface.dart +++ b/lib/rtmp_stream_platform_interface.dart @@ -1,3 +1,4 @@ +import 'package:haishin_kit/av_capture_session_preset.dart'; import 'package:haishin_kit/rtmp_stream_method_channel.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; @@ -17,21 +18,46 @@ abstract class RtmpStreamPlatform extends PlatformInterface { _instance = instance; } + /// Gets the hasAudio property. + Future getHasAudio() { + throw UnimplementedError('getHasAudio() has not been implemented.'); + } + + /// Sets the hasAudio property. + Future setHasAudio(Map params) { + throw UnimplementedError('setHasAudio() has not been implemented.'); + } + + /// Gets the hasVideo property. + Future getHasVideo() { + throw UnimplementedError('getHasVideo() has not been implemented.'); + } + + /// Sets the hasVideo property. + Future setHasVideo(Map params) { + throw UnimplementedError('setHasVideo() has not been implemented.'); + } + + /// Sets the frameRate property. + Future setFrameRate(Map params) { + throw UnimplementedError('setFrameRate() has not been implemented.'); + } + + /// Sets the sessionPreset property. + Future setSessionPreset(Map params) { + throw UnimplementedError('setSessionPreset has not been implemented.'); + } + /// Sets the audio decoding properties. Future setAudioSettings(Map params) { throw UnimplementedError('setAudioSettings() has not been implemented.'); } - /// Sets the video decoding properties. + /// Sets the sessionPreset for the AVCaptureSession. Future setVideoSettings(Map params) { throw UnimplementedError('setVideoSettings() has not been implemented.'); } - /// Sets the AVCaptureSession(iOS) properties. - Future setCaptureSettings(Map params) { - throw UnimplementedError('setCaptureSettings() has not been implemented.'); - } - /// Attaches an audio source. Future attachAudio(Map params) { throw UnimplementedError('attachAudio() has not been implemented.'); diff --git a/lib/video_settings.dart b/lib/video_settings.dart index 8a44eb9..a6ae88b 100644 --- a/lib/video_settings.dart +++ b/lib/video_settings.dart @@ -1,51 +1,51 @@ enum ProfileLevel { - H264Baseline31('H264_Baseline_3_1'), - H264Baseline32('H264_Baseline_3_2'), - H264Baseline40('H264_Baseline_4_0'), - H264Baseline41('H264_Baseline_4_1'), - H264Baseline42('H264_Baseline_4_2'), - H264Baseline50('H264_Baseline_5_0'), - H264Baseline51('H264_Baseline_5_1'), - H264Baseline52('H264_Baseline_5_2'), - H264High31('H264_High_3_1'), - H264High32('H264_High_3_2'), - H264High40('H264_High_4_0'), - H264High41('H264_High_4_1'), - H264High42('H264_High_4_2'), - H264High50('H264_High_5_0'), - H264High51('H264_High_5_1'), - H264High52('H264_High_5_2'), - H264Main31('H264_Main_3_1'), - H264Main32('H264_Main_3_2'), - H264Main40('H264_Main_4_0'), - H264Main41('H264_Main_4_1'), - H264Main42('H264_Main_4_2'), - H264Main50('H264_Main_5_0'), - H264Main51('H264_Main_5_1'), - H264Main52('H264_Main_5_2'), + h264Baseline31('H264_Baseline_3_1'), + h264Baseline32('H264_Baseline_3_2'), + h264Baseline40('H264_Baseline_4_0'), + h264Baseline41('H264_Baseline_4_1'), + h264Baseline42('H264_Baseline_4_2'), + h264Baseline50('H264_Baseline_5_0'), + h264Baseline51('H264_Baseline_5_1'), + h264Baseline52('H264_Baseline_5_2'), + h264High31('H264_High_3_1'), + h264High32('H264_High_3_2'), + h264High40('H264_High_4_0'), + h264High41('H264_High_4_1'), + h264High42('H264_High_4_2'), + h264High50('H264_High_5_0'), + h264High51('H264_High_5_1'), + h264High52('H264_High_5_2'), + h264Main31('H264_Main_3_1'), + h264Main32('H264_Main_3_2'), + h264Main40('H264_Main_4_0'), + h264Main41('H264_Main_4_1'), + h264Main42('H264_Main_4_2'), + h264Main50('H264_Main_5_0'), + h264Main51('H264_Main_5_1'), + h264Main52('H264_Main_5_2'), // The following values are supported only on the iOS. - H264Baseline13('H264_Baseline_1_3'), - H264Baseline30('H264_Baseline_3_0'), - H264Extended50('H264_Extended_5_0'), - H264ExtendedAutoLevel('H264_Extended_AutoLevel'), - H264High30('H264_High_3_0'), - H264Main30('H264_Main_3_0'), - H264BaselineAutoLevel('H264_Baseline_AutoLevel'), - H264MainAutoLevel('H264_Main_AutoLevel'), - H264HighAutoLevel('H264_High_AutoLevel'), + h264Baseline13('H264_Baseline_1_3'), + h264Baseline30('H264_Baseline_3_0'), + h264Extended50('H264_Extended_5_0'), + h264ExtendedAutoLevel('H264_Extended_AutoLevel'), + h264High30('H264_High_3_0'), + h264Main30('H264_Main_3_0'), + h264BaselineAutoLevel('H264_Baseline_AutoLevel'), + h264MainAutoLevel('H264_Main_AutoLevel'), + h264HighAutoLevel('H264_High_AutoLevel'), // The following values are supported only on the iOS 15.0 and above - H264ConstrainedBaselineAutoLevel('H264_ConstrainedBaseline_AutoLevel'), - H264ConstrainedHighAutoLevel('H264_ConstrainedHigh_AutoLevel'), + h264ConstrainedBaselineAutoLevel('H264_ConstrainedBaseline_AutoLevel'), + h264ConstrainedHighAutoLevel('H264_ConstrainedHigh_AutoLevel'), ; const ProfileLevel(this.profileLevelName); + final String profileLevelName; } class VideoSettings { - bool muted; int width; int height; int bitrate; @@ -55,12 +55,11 @@ class VideoSettings { // VideoSettings({ - this.muted = false, this.width = 480, this.height = 272, this.bitrate = 160 * 1000, this.frameInterval = 2, - this.profileLevel = ProfileLevel.H264Baseline31, + this.profileLevel = ProfileLevel.h264Baseline31, }); @override @@ -68,7 +67,6 @@ class VideoSettings { identical(this, other) || (other is VideoSettings && runtimeType == other.runtimeType && - muted == other.muted && width == other.width && height == other.height && bitrate == other.bitrate && @@ -77,7 +75,6 @@ class VideoSettings { @override int get hashCode => - muted.hashCode ^ width.hashCode ^ height.hashCode ^ bitrate.hashCode ^ @@ -86,18 +83,10 @@ class VideoSettings { @override String toString() { - return 'VideoSettings{' + - ' muted: $muted,' + - ' width: $width,' + - ' height: $height,' + - ' bitrate: $bitrate,' + - ' frameInterval: $frameInterval,' + - ' profileLevel: ${profileLevel.profileLevelName},' + - '}'; + return 'VideoSettings{width: $width, height: $height, bitrate: $bitrate, frameInterval: $frameInterval, profileLevel: ${profileLevel.profileLevelName}}'; } VideoSettings copyWith({ - bool? muted, int? width, int? height, int? bitrate, @@ -105,7 +94,6 @@ class VideoSettings { ProfileLevel? profileLevel, }) { return VideoSettings( - muted: muted ?? this.muted, width: width ?? this.width, height: height ?? this.height, bitrate: bitrate ?? this.bitrate, @@ -116,18 +104,16 @@ class VideoSettings { Map toMap() { return { - 'muted': this.muted, - 'width': this.width, - 'height': this.height, - 'bitrate': this.bitrate, - 'frameInterval': this.frameInterval, - 'profileLevel': this.profileLevel.profileLevelName, + 'width': width, + 'height': height, + 'bitrate': bitrate, + 'frameInterval': frameInterval, + 'profileLevel': profileLevel.profileLevelName, }; } factory VideoSettings.fromMap(Map map) { return VideoSettings( - muted: map['muted'] as bool, width: map['width'] as int, height: map['height'] as int, bitrate: map['bitrate'] as int, diff --git a/lib/video_source.dart b/lib/video_source.dart index f41c32d..c4e9bd2 100644 --- a/lib/video_source.dart +++ b/lib/video_source.dart @@ -21,7 +21,7 @@ class VideoSource { @override String toString() { - return 'VideoSource{' + ' position: $position,' + '}'; + return 'VideoSource{position: $position}'; } VideoSource copyWith({ diff --git a/pubspec.yaml b/pubspec.yaml index e86efa7..0c16aa7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: haishin_kit description: A Flutter plugin for Camera and Microphone streaming library via RTMP. -version: 0.10.0 +version: 0.11.0 homepage: https://github.com/shogo4405/HaishinKit.dart environment: