diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..e5c0921 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,35 @@ +name: Build +on: [push] + +defaults: + run: + working-directory: example/ + +jobs: + build_ios: + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Flutter + id: flutter + uses: DanTup/gh-actions/setup-flutter@master + with: + channel: stable + - name: Install dependencies + run: flutter pub get + - name: Build iOS application + run: flutter build ios --no-codesign + + build_android: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Flutter + id: flutter + uses: DanTup/gh-actions/setup-flutter@master + with: + channel: 2.5.3 + - name: Install dependencies + run: flutter pub get + - name: Build Android application + run: flutter build apk \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f1c8099 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,18 @@ +name: Publish to pub.dev +on: + release: + types: [ published ] +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Flutter + id: flutter + uses: DanTup/gh-actions/setup-flutter@master + with: + channel: stable + - name: Install dependencies + run: flutter pub get + - name: Publishing + run: flutter pub publish \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..27e8946 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,32 @@ +name: Run tests +on: [push] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Flutter + id: flutter + uses: DanTup/gh-actions/setup-flutter@master + with: + channel: stable + - name: Install dependencies + run: flutter pub get + - name: Analyze project source + run: flutter analyze + - name: Run tests + run: flutter test + + publish_test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Flutter + id: flutter + uses: DanTup/gh-actions/setup-flutter@master + with: + channel: stable + - name: Install dependencies + run: flutter pub get + - name: Dry-run publishing + run: flutter pub publish --dry-run \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4a8d516..d193fc1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,38 +1,74 @@ +# Miscellaneous +*.class +*.lock +*.log +*.pyc +*.swp .DS_Store -.dart_tool/ +.atom/ +.buildlog/ +.history +.svn/ -.packages -.pub/ +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ -build/ +# Visual Studio Code related +.classpath +.project +.settings/ +.vscode/ + +# Flutter repo-specific +/bin/cache/ +/bin/internal/bootstrap.bat +/bin/internal/bootstrap.sh +/bin/mingit/ +/dev/benchmarks/mega_gallery/ +/dev/bots/.recipe_deps +/dev/bots/android_tools/ +/dev/devicelab/ABresults*.json +/dev/docs/doc/ +/dev/docs/flutter.docs.zip +/dev/docs/lib/ +/dev/docs/pubspec.yaml +/dev/integration_tests/**/xcuserdata +/dev/integration_tests/**/Pods +/packages/flutter/coverage/ +version +analysis_benchmark.json + +# packages file containing multi-root paths +.packages.generated -### Flutter ### # Flutter/Dart/Pub related **/doc/api/ .dart_tool/ .flutter-plugins .flutter-plugins-dependencies -.fvm/ +**/generated_plugin_registrant.dart .packages .pub-cache/ .pub/ build/ -coverage/ -lib/generated_plugin_registrant.dart -# For library packages, don’t commit the pubspec.lock file. -# Regenerating the pubspec.lock file lets you test your package against the latest compatible versions of its dependencies. -# See https://dart.dev/guides/libraries/private-files#pubspeclock -#pubspec.lock +flutter_*.png +linked_*.ds +unlinked.ds +unlinked_spec.ds # Android related **/android/**/gradle-wrapper.jar -**/android/.gradle +.gradle/ **/android/captures/ **/android/gradlew **/android/gradlew.bat -**/android/key.properties **/android/local.properties **/android/**/GeneratedPluginRegistrant.java +**/android/key.properties +*.jks # iOS/XCode related **/ios/**/*.mode1v3 @@ -56,6 +92,7 @@ lib/generated_plugin_registrant.dart **/ios/Flutter/Flutter.framework **/ios/Flutter/Flutter.podspec **/ios/Flutter/Generated.xcconfig +**/ios/Flutter/ephemeral **/ios/Flutter/app.flx **/ios/Flutter/app.zip **/ios/Flutter/flutter_assets/ @@ -63,183 +100,19 @@ lib/generated_plugin_registrant.dart **/ios/ServiceDefinitions.json **/ios/Runner/GeneratedPluginRegistrant.* +# macOS +**/macos/Flutter/GeneratedPluginRegistrant.swift + +# Coverage +coverage/ + +# Symbols +app.*.symbols + # Exceptions to above rules. !**/ios/**/default.mode1v3 !**/ios/**/default.mode2v3 !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages - -### Xcode ### -# Xcode -# -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore - -## User settings -xcuserdata/ - -## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) -*.xcscmblueprint -*.xccheckout - -## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) -DerivedData/ -*.moved-aside -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 - -## Gcc Patch -/*.gcno - -### Xcode Patch ### -*.xcodeproj/* -!*.xcodeproj/project.pbxproj -!*.xcodeproj/xcshareddata/ -!*.xcworkspace/contents.xcworkspacedata -**/xcshareddata/WorkspaceSettings.xcsettings - -### AndroidStudio ### -# Covers files to be ignored for android development using Android Studio. - -# Built application files -*.apk -*.ap_ -*.aab - -# Files for the ART/Dalvik VM -*.dex - -# Java class files -*.class - -# Generated files -bin/ -gen/ -out/ - -# Gradle files -.gradle -.gradle/ - -# Signing files -.signing/ - -# Local configuration file (sdk path, etc) -local.properties - -# Proguard folder generated by Eclipse -proguard/ - -# Log Files -*.log - -# Android Studio -/*/build/ -/*/local.properties -/*/out -/*/*/build -/*/*/production -captures/ -.navigation/ -*.ipr -*~ -*.swp - -# Keystore files -*.jks -*.keystore - -# Google Services (e.g. APIs or Firebase) -# google-services.json - -# Android Patch -gen-external-apklibs - -# External native build folder generated in Android Studio 2.2 and later -.externalNativeBuild - -# NDK -obj/ - -# IntelliJ IDEA -*.iml -*.iws -/out/ - -# User-specific configurations -.idea/caches/ -.idea/libraries/ -.idea/shelf/ -.idea/workspace.xml -.idea/tasks.xml -.idea/.name -.idea/compiler.xml -.idea/copyright/profiles_settings.xml -.idea/encodings.xml -.idea/misc.xml -.idea/modules.xml -.idea/scopes/scope_settings.xml -.idea/dictionaries -.idea/vcs.xml -.idea/jsLibraryMappings.xml -.idea/datasources.xml -.idea/dataSources.ids -.idea/sqlDataSources.xml -.idea/dynamic.xml -.idea/uiDesigner.xml -.idea/assetWizardSettings.xml -.idea/gradle.xml -.idea/jarRepositories.xml -.idea/navEditor.xml - -# OS-specific files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# Legacy Eclipse project files -.classpath -.project -.cproject -.settings/ - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.war -*.ear - -# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) -hs_err_pid* - -## Plugin-specific files: - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Mongo Explorer plugin -.idea/mongoSettings.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -### AndroidStudio Patch ### - -!/gradle/wrapper/gradle-wrapper.jar +!/dev/ci/**/Gemfile.lock diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/runConfigurations/example_lib_main_dart.xml b/.idea/runConfigurations/example_lib_main_dart.xml deleted file mode 100644 index 5fd9159..0000000 --- a/.idea/runConfigurations/example_lib_main_dart.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 41cc7d8..6073234 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,3 @@ -## 0.0.1 +## 0.1.0 -* TODO: Describe initial release. +* Initial release. diff --git a/LICENSE b/LICENSE index ba75c69..c0eef12 100644 --- a/LICENSE +++ b/LICENSE @@ -1 +1,21 @@ -TODO: Add your license here. +MIT License + +Copyright (c) 2022 api.video + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/android/build.gradle b/android/build.gradle index 42c9e79..f15ccc6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,15 +1,15 @@ -group 'video.api.eco.flt.livestream.apivideolivestream' +group 'video.api.flutter.livestream' version '1.0-SNAPSHOT' buildscript { ext.kotlin_version = '1.3.50' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.0.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -17,7 +17,7 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() maven { url 'https://jitpack.io' } } } @@ -39,5 +39,5 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.constraintlayout:constraintlayout:2.1.0' - implementation 'video.api:android-live-stream:0.1.5' + implementation 'video.api:android-live-stream:0.3.1' } diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 7fcb384..2e3d9f4 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,7 @@ + package="video.api.flutter.livestream"> + + + + diff --git a/android/src/main/kotlin/video/api/eco/flt/livestream/apivideolivestream/LiveStreamNativeView.kt b/android/src/main/kotlin/video/api/eco/flt/livestream/apivideolivestream/LiveStreamNativeView.kt deleted file mode 100644 index 5143ef6..0000000 --- a/android/src/main/kotlin/video/api/eco/flt/livestream/apivideolivestream/LiveStreamNativeView.kt +++ /dev/null @@ -1,109 +0,0 @@ -package video.api.eco.flt.livestream.apivideolivestream - -import android.content.Context -import android.graphics.Camera -import android.util.Log -import android.view.View -import com.pedro.encoder.input.video.CameraHelper -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.platform.PlatformView -import net.ossrs.rtmp.ConnectCheckerRtmp -import video.api.livestream_module.ApiVideoLiveStream -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import java.lang.Exception - -class LiveStreamNativeView(context: Context, id: Int, creationParams: Map?, messenger: BinaryMessenger): - PlatformView, ConnectCheckerRtmp, MethodCallHandler { - - //private var channel : MethodChannel = MethodChannel(messenger, "apivideolivestream") - - private lateinit var view: LiveStreamView - private var apiVideo: ApiVideoLiveStream - private var livestreamKey: String = "" - private var url : String? = null - private var methodChannel: MethodChannel? = null - - override fun getView(): View { - return view.findViewById(R.id.opengl_view) - } - override fun dispose() { - try { - methodChannel?.setMethodCallHandler(null) - }catch (e: Exception){ - Log.e("MethodCallHandler","Already null") - } - } - - init { - //channel.setMethodCallHandler(this) - view = LiveStreamView(context) - apiVideo = ApiVideoLiveStream(context, this, null, null) - initMethodChannel(messenger, id) - } - - private fun initMethodChannel(messenger: BinaryMessenger, viewId: Int){ - methodChannel = MethodChannel(messenger, "apivideolivestream_$viewId") - methodChannel!!.setMethodCallHandler(this) - } - - override fun onConnectionSuccessRtmp() { - Log.i("Rtmp Connection", "success") - } - - override fun onConnectionFailedRtmp(reason: String?) { - Log.e("Rtmp Connection", "failed") - } - - override fun onNewBitrateRtmp(bitrate: Long) { - Log.i("New rtmp bitrate", "$bitrate") - } - - override fun onDisconnectRtmp() { - Log.i("Rtmp connetion", "On disconnect") - } - - override fun onAuthErrorRtmp() { - Log.e("Rtmp Auth", "error") - } - - override fun onAuthSuccessRtmp() { - Log.i("Rtmp Auth", "success") - } - - private fun setUrl(newUrl: String?){ - url = if(url != null || url != ""){ - newUrl - }else{ - null - } - } - - private fun startLive(){ - Log.e("startlive method","called") - Log.e("startlive key",livestreamKey) - apiVideo.startStreaming(livestreamKey, url) - } - private fun stopLive(){ - Log.e("stop method","called") - apiVideo.stopStreaming() - } - private fun switchCamera(){ - Log.e("camera switch", apiVideo.videoCamera.toString()) - if(apiVideo.videoCamera === CameraHelper.Facing.BACK){ - apiVideo.videoCamera = CameraHelper.Facing.FRONT - }else{ - apiVideo.videoCamera = CameraHelper.Facing.BACK - } - } - - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - when (call.method){ - "setLivestreamKey" -> livestreamKey = call.arguments.toString() - "startStreaming" -> startLive() - "stopStreaming" -> stopLive() - "switchCamera" -> switchCamera() - } - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/video/api/eco/flt/livestream/apivideolivestream/LiveStreamView.kt b/android/src/main/kotlin/video/api/eco/flt/livestream/apivideolivestream/LiveStreamView.kt deleted file mode 100644 index 9c1c0b8..0000000 --- a/android/src/main/kotlin/video/api/eco/flt/livestream/apivideolivestream/LiveStreamView.kt +++ /dev/null @@ -1,10 +0,0 @@ -package video.api.eco.flt.livestream.apivideolivestream - -import android.content.Context -import androidx.constraintlayout.widget.ConstraintLayout - -class LiveStreamView(context: Context): ConstraintLayout(context) { - init { - inflate(context, R.layout.fluter_livestream, this) - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/video/api/eco/flt/livestream/apivideolivestream/NativeViewFactory.kt b/android/src/main/kotlin/video/api/eco/flt/livestream/apivideolivestream/NativeViewFactory.kt deleted file mode 100644 index f4bbf69..0000000 --- a/android/src/main/kotlin/video/api/eco/flt/livestream/apivideolivestream/NativeViewFactory.kt +++ /dev/null @@ -1,19 +0,0 @@ -package video.api.eco.flt.livestream.apivideolivestream - -import android.content.Context -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.StandardMessageCodec -import io.flutter.plugin.platform.PlatformView -import io.flutter.plugin.platform.PlatformViewFactory - -class NativeViewFactory(private val messenger: BinaryMessenger): PlatformViewFactory( - StandardMessageCodec.INSTANCE) { - private lateinit var mess : BinaryMessenger - - override fun create(context: Context, viewId: Int, args: Any?): PlatformView { - val creationParams = args as Map? - this.mess = messenger - return LiveStreamNativeView(context, viewId, creationParams, this.mess) - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/video/api/eco/flt/livestream/apivideolivestream/ApivideolivestreamPlugin.kt b/android/src/main/kotlin/video/api/flutter/livestream/ApiVideoLiveStreamPlugin.kt similarity index 54% rename from android/src/main/kotlin/video/api/eco/flt/livestream/apivideolivestream/ApivideolivestreamPlugin.kt rename to android/src/main/kotlin/video/api/flutter/livestream/ApiVideoLiveStreamPlugin.kt index b5852a1..87c0746 100644 --- a/android/src/main/kotlin/video/api/eco/flt/livestream/apivideolivestream/ApivideolivestreamPlugin.kt +++ b/android/src/main/kotlin/video/api/flutter/livestream/ApiVideoLiveStreamPlugin.kt @@ -1,34 +1,21 @@ -package video.api.eco.flt.livestream.apivideolivestream +package video.api.flutter.livestream -import android.util.Log import androidx.annotation.NonNull - import io.flutter.embedding.engine.plugins.FlutterPlugin -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 -/** ApivideolivestreamPlugin */ -class ApivideolivestreamPlugin: FlutterPlugin{ +/** ApiVideoLiveStreamPlugin */ +class ApiVideoLiveStreamPlugin: FlutterPlugin{ /// The MethodChannel that will the communication between Flutter and native Android /// /// This local reference serves to register the plugin with the Flutter Engine and unregister it /// when the Flutter Engine is detached from the Activity - //private lateinit var channel : MethodChannel override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - //channel = MethodChannel(flutterPluginBinding.binaryMessenger, "apivideolivestream1") - //channel.setMethodCallHandler(this) flutterPluginBinding .platformViewRegistry .registerViewFactory("", NativeViewFactory(flutterPluginBinding.binaryMessenger)) } - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { -/* - channel.setMethodCallHandler(null) -*/ } } diff --git a/android/src/main/kotlin/video/api/flutter/livestream/ConfigHelper.kt b/android/src/main/kotlin/video/api/flutter/livestream/ConfigHelper.kt new file mode 100644 index 0000000..eba417a --- /dev/null +++ b/android/src/main/kotlin/video/api/flutter/livestream/ConfigHelper.kt @@ -0,0 +1,17 @@ +package video.api.flutter.livestream + +import video.api.livestream.enums.Resolution + +object ConfigHelper { + fun getResolutionFromResolutionString(resolutionString: String): Resolution { + return when (resolutionString) { + "240p" -> Resolution.RESOLUTION_240 + "360p" -> Resolution.RESOLUTION_360 + "480p" -> Resolution.RESOLUTION_480 + "720p" -> Resolution.RESOLUTION_720 + "1080p" -> Resolution.RESOLUTION_1080 + "2160p" -> Resolution.RESOLUTION_2160 + else -> Resolution.RESOLUTION_720 + } + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/video/api/flutter/livestream/Extensions.kt b/android/src/main/kotlin/video/api/flutter/livestream/Extensions.kt new file mode 100644 index 0000000..5b48508 --- /dev/null +++ b/android/src/main/kotlin/video/api/flutter/livestream/Extensions.kt @@ -0,0 +1,24 @@ +package video.api.flutter.livestream + +import video.api.livestream.models.AudioConfig +import video.api.livestream.models.VideoConfig + +fun Map.toVideoConfig(): VideoConfig { + return VideoConfig( + bitrate = this["bitrate"] as Int, + resolution = ConfigHelper.getResolutionFromResolutionString(this["resolution"] as String), + fps = this["fps"] as Int + ) +} + +fun Map.toAudioConfig(): AudioConfig { + return AudioConfig( + bitrate = this["bitrate"] as Int, + sampleRate = this["sampleRate"] as Int, + stereo = this["channel"] == "stereo", + noiseSuppressor = this["enableNoiseSuppressor"] as Boolean, + echoCanceler = this["enableEchoCanceler"] as Boolean + ) +} + + diff --git a/android/src/main/kotlin/video/api/flutter/livestream/LiveStreamNativeView.kt b/android/src/main/kotlin/video/api/flutter/livestream/LiveStreamNativeView.kt new file mode 100644 index 0000000..ac6070d --- /dev/null +++ b/android/src/main/kotlin/video/api/flutter/livestream/LiveStreamNativeView.kt @@ -0,0 +1,139 @@ +package video.api.flutter.livestream + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.util.Log +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.platform.PlatformView +import video.api.livestream.ApiVideoLiveStream +import video.api.livestream.enums.CameraFacingDirection +import video.api.livestream.interfaces.IConnectionChecker +import video.api.livestream.views.ApiVideoView + + +class LiveStreamNativeView( + context: Context, + id: Int, + messenger: BinaryMessenger, + creationParams: Map +) : + PlatformView, IConnectionChecker, MethodCallHandler { + companion object { + const val TAG = "LiveStreamNativeView" + } + + private val glView = ApiVideoView(context) + private var liveStream = + ApiVideoLiveStream( + context = context, + connectionChecker = this, + initialAudioConfig = (creationParams["audioParameters"] as Map).toAudioConfig(), + initialVideoConfig = (creationParams["videoParameters"] as Map).toVideoConfig(), + apiVideoView = glView + ) + private var methodChannel = MethodChannel(messenger, "video.api.livestream/controller") + + override fun getView() = glView + + override fun dispose() { + try { + liveStream.stopStreaming() + methodChannel.setMethodCallHandler(null) + } catch (e: Exception) { + Log.e(TAG, "Already null") + } + } + + init { + methodChannel.setMethodCallHandler(this) + } + + override fun onConnectionSuccess() { + Handler(Looper.getMainLooper()).post { + methodChannel.invokeMethod("onConnectionSuccess", null) + } + } + + override fun onConnectionFailed(reason: String) { + Handler(Looper.getMainLooper()).post { + methodChannel.invokeMethod("onConnectionError", reason) + } + } + + override fun onConnectionStarted(url: String) { + Log.d(TAG, "onConnectionStarted") + } + + override fun onDisconnect() { + Handler(Looper.getMainLooper()).post { + methodChannel.invokeMethod("onDisconnect", null) + } + } + + override fun onAuthError() { + Log.e(TAG, "Rtmp Auth error") + } + + override fun onAuthSuccess() { + Log.d(TAG, "Rtmp Auth success") + } + + private fun setVideoParameters(map: Map) { + liveStream.videoConfig = map.toVideoConfig() + } + + private fun setAudioParameters(map: Map) { + liveStream.audioConfig = map.toAudioConfig() + } + + private fun startStreaming(streamKey: String, url: String) { + Log.d(TAG, "startlive method called") + liveStream.startStreaming(streamKey, url) + } + + private fun stopStreaming() { + Log.d(TAG, "stop method called") + liveStream.stopStreaming() + } + + private fun switchCamera() { + Log.d(TAG, "camera switch") + if (liveStream.camera == CameraFacingDirection.BACK) { + liveStream.camera = CameraFacingDirection.FRONT + } else { + liveStream.camera = CameraFacingDirection.BACK + } + } + + private fun toggleMute() { + Log.d(TAG, "toggle microphone") + liveStream.isMuted = !liveStream.isMuted + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "startStreaming" -> { + val streamKey = call.argument("streamKey") + val url = call.argument("url") + when { + streamKey == null -> result.error("missing_stream_key", "Stream key is missing", null) + url == null -> result.error("missing_rtmp_url", "RTMP url is missing", null) + else -> startStreaming(streamKey, url) + } + } + "stopStreaming" -> stopStreaming() + "setVideoParameters" -> { + setVideoParameters(call.arguments as Map) + } + "setAudioParameters" -> { + setAudioParameters(call.arguments as Map) + } + "switchCamera" -> switchCamera() + "toggleMute" -> toggleMute() + } + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/video/api/flutter/livestream/NativeViewFactory.kt b/android/src/main/kotlin/video/api/flutter/livestream/NativeViewFactory.kt new file mode 100644 index 0000000..7d5d621 --- /dev/null +++ b/android/src/main/kotlin/video/api/flutter/livestream/NativeViewFactory.kt @@ -0,0 +1,17 @@ +package video.api.flutter.livestream + +import android.content.Context +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.StandardMessageCodec +import io.flutter.plugin.platform.PlatformView +import io.flutter.plugin.platform.PlatformViewFactory + +class NativeViewFactory(private val messenger: BinaryMessenger) : PlatformViewFactory( + StandardMessageCodec.INSTANCE +) { + override fun create(context: Context, viewId: Int, args: Any?): PlatformView { + val creationParams = args as Map + return LiveStreamNativeView(context, viewId, messenger, creationParams) + } +} \ No newline at end of file diff --git a/android/src/main/res/layout/fluter_livestream.xml b/android/src/main/res/layout/fluter_livestream.xml deleted file mode 100644 index 94b9e83..0000000 --- a/android/src/main/res/layout/fluter_livestream.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - \ No newline at end of file diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index fd5c5ad..5a41421 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -34,7 +34,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "video.api.eco.flt.livestream.apivideolivestream_example" + applicationId "video.api.flutter.livestream.example" minSdkVersion 21 targetSdkVersion 30 versionCode flutterVersionCode.toInteger() diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml index e8f02aa..6f8e8c8 100644 --- a/example/android/app/src/debug/AndroidManifest.xml +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="video.api.flutter.livestream.example"> diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index a3437ad..011c2a1 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,10 +1,10 @@ + package="video.api.flutter.livestream.example"> + package="video.api.flutter.livestream.example"> diff --git a/example/android/build.gradle b/example/android/build.gradle index 9b6ed06..530764e 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.6.0' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.0.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -14,7 +14,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index bc6a58a..b1fe44c 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Jun 23 08:50:38 CEST 2017 +#Tue Dec 21 16:00:47 CET 2021 distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +zipStoreBase=GRADLE_USER_HOME diff --git a/example/ios/Podfile b/example/ios/Podfile index 9411102..ee6a180 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -37,5 +37,16 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ + '$(inherited)', + + ## dart: PermissionGroup.camera + 'PERMISSION_CAMERA=1', + + ## dart: PermissionGroup.microphone + 'PERMISSION_MICROPHONE=1', + ] + end end end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock deleted file mode 100644 index 44435d3..0000000 --- a/example/ios/Podfile.lock +++ /dev/null @@ -1,43 +0,0 @@ -PODS: - - apivideolivestream (0.0.1): - - Flutter - - LiveStreamIos (~> 0.0.2) - - Flutter (1.0.0) - - HaishinKit (1.1.5): - - Logboard (~> 2.2.1) - - image_picker (0.0.1): - - Flutter - - LiveStreamIos (0.0.2): - - HaishinKit (~> 1.1.0) - - Logboard (2.2.2) - -DEPENDENCIES: - - apivideolivestream (from `.symlinks/plugins/apivideolivestream/ios`) - - Flutter (from `Flutter`) - - image_picker (from `.symlinks/plugins/image_picker/ios`) - -SPEC REPOS: - trunk: - - HaishinKit - - LiveStreamIos - - Logboard - -EXTERNAL SOURCES: - apivideolivestream: - :path: ".symlinks/plugins/apivideolivestream/ios" - Flutter: - :path: Flutter - image_picker: - :path: ".symlinks/plugins/image_picker/ios" - -SPEC CHECKSUMS: - apivideolivestream: 8dea154ed1756fca177fe310d81d50b49e8ac58e - Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a - HaishinKit: 5cf385a823dee6b299404877edf418974422bf9f - image_picker: e06f7a68f000bd36f552c1847e33cda96ed31f1f - LiveStreamIos: dffd0e5d6f9fb712f72ac87eb018e0396bc1284e - Logboard: 0ab6bbd984ed032b3f0b615cef06779a73445c80 - -PODFILE CHECKSUM: fe0e1ee7f3d1f7d00b11b474b62dd62134535aea - -COCOAPODS: 1.10.1 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 2ba3492..32324c5 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -355,14 +355,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = VY3VXRC7P4; + DEVELOPMENT_TEAM = GBC36KP98K; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = video.api.eco.flt.livestream.apivideolivestreamExample; + PRODUCT_BUNDLE_IDENTIFIER = video.api.flutter.livestream.Example; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -484,14 +484,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = VY3VXRC7P4; + DEVELOPMENT_TEAM = GBC36KP98K; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = video.api.eco.flt.livestream.apivideolivestreamExample; + PRODUCT_BUNDLE_IDENTIFIER = video.api.flutter.livestream.Example; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -507,14 +507,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = VY3VXRC7P4; + DEVELOPMENT_TEAM = GBC36KP98K; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = video.api.eco.flt.livestream.apivideolivestreamExample; + PRODUCT_BUNDLE_IDENTIFIER = video.api.flutter.livestream.Example; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index cb30c04..833a1b3 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -22,6 +22,12 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + NSCameraUsageDescription + Your own description of the purpose + NSMicrophoneUsageDescription + Your own description of the purpose + NSPhotoLibraryUsageDescription + This app need access to your library UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,11 +47,5 @@ UIViewControllerBasedStatusBarAppearance - NSPhotoLibraryUsageDescription - This app need access to your library - NSCameraUsageDescription - Your own description of the purpose - NSMicrophoneUsageDescription - Your own description of the purpose diff --git a/example/lib/constants.dart b/example/lib/constants.dart new file mode 100644 index 0000000..825a928 --- /dev/null +++ b/example/lib/constants.dart @@ -0,0 +1,7 @@ +class Constants { + static const String Settings = "Settings"; + + static const List choices = [ + Settings, + ]; +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 269c84d..9134231 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,61 +1,101 @@ +import 'package:apivideo_live_stream/apivideo_live_stream.dart'; +import 'package:apivideo_live_stream_example/settings_screen.dart'; +import 'package:apivideo_live_stream_example/types/params.dart'; import 'package:flutter/material.dart'; -import 'package:apivideolivestream/apivideolivestream.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import 'constants.dart'; + +const permissions = [Permission.camera, Permission.microphone]; void main() { runApp(MyApp()); } -class MyApp extends StatefulWidget { +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return new MaterialApp(home: LiveViewPage()); + } +} + +class LiveViewPage extends StatefulWidget { + const LiveViewPage({Key? key}) : super(key: key); + @override - _MyAppState createState() => _MyAppState(); + _LiveViewPageState createState() => new _LiveViewPageState(); } -class _MyAppState extends State { - Apivideolivestream? controller; +class _LiveViewPageState extends State { + final ButtonStyle buttonStyle = + ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)); + bool _isStreaming = false; - String _title = "Start"; + String _liveButtonTitle = "Start"; + String _rtmpStreamKey = ''; + Params params = Params(); + final LiveStreamController _controller = + LiveStreamController(onConnectionError: (error) { + // TODO: change live button state + }); @override void initState() { + // TODO: implement initState super.initState(); } @override Widget build(BuildContext context) { - final ButtonStyle style = - ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)); - - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), - body: Center( - child: Column( - children: [ - Center( - child: _cameraPreviewWidget(), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - ElevatedButton( - style: style, - onPressed: () { - Apivideolivestream.switchCamera(); - }, - child: const Text('switch'), - ), - const SizedBox(width: 30), - ElevatedButton( - style: style, - onPressed: _toggleStream, - child: Text('$_title'), - ), - ], - ) - ], - ), + return Scaffold( + appBar: AppBar( + title: const Text('Live Stream Example'), + actions: [ + PopupMenuButton( + onSelected: (choice) => _onMenuSelected(choice, context), + itemBuilder: (BuildContext context) { + return Constants.choices.map((String choice) { + return PopupMenuItem( + value: choice, + child: Text(choice), + ); + }).toList(); + }, + ) + ], + ), + body: Center( + child: Column( + children: [ + Center( + child: SizedBox( + height: 400, + child: CameraContainer( + controller: _controller, + initialVideoParameters: params.video, + initialAudioParameters: params.audio))), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton( + style: buttonStyle, + onPressed: () { + _controller.switchCamera(); + }, + child: const Text('switch'), + ), + const SizedBox(width: 30), + ElevatedButton( + style: buttonStyle, + onPressed: _toggleStream, + child: Text('$_liveButtonTitle'), + ), + ], + ), + Center( + child: Text('$_rtmpStreamKey'), + ) + ], ), ), ); @@ -65,28 +105,92 @@ class _MyAppState extends State { setState(() { if (_isStreaming) { print("Stop Stream"); - _title = "Start"; + _liveButtonTitle = "Start"; _isStreaming = false; - Apivideolivestream.stopStream(); + _controller.stopStreaming(); } else { print("Start Stream"); - _title = "Stop"; + _liveButtonTitle = "Stop"; _isStreaming = true; - Apivideolivestream.startStream(); + _controller.startStreaming( + streamKey: params.streamKey, url: params.rtmpUrl); } }); } - Widget _cameraPreviewWidget() { - final plugin = Apivideolivestream(); + void _onMenuSelected(String choice, BuildContext context) { + if (choice == Constants.Settings) { + _awaitResultFromSettingsFinal(context); + } + } - return Container( - color: Colors.lightBlueAccent, - child: LiveStreamPreview( - controller: plugin, - liveStreamKey: 'd08c582e-e251-4f9e-9894-8c8d69755d45', - videoResolution: '1080p', - ), - ); + void _awaitResultFromSettingsFinal(BuildContext context) async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SettingsScreen(params: params))); + setState(() { + _rtmpStreamKey = params.streamKey; + }); + _controller.setVideoParameters(params.video); + _controller.setAudioParameters(params.audio); + } +} + +class CameraContainer extends StatelessWidget { + final LiveStreamController controller; + final VideoParameters initialVideoParameters; + final AudioParameters initialAudioParameters; + + CameraContainer( + {required this.controller, + required this.initialVideoParameters, + required this.initialAudioParameters}); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _requestPermission(permissions), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (!snapshot.hasData) { + // while data is loading: + return Center( + child: CircularProgressIndicator(), + ); + } else { + final hasPermissionsAccepted = snapshot.data!; + if (hasPermissionsAccepted) { + return CameraPreview( + controller: controller, + initialVideoParameters: initialVideoParameters, + initialAudioParameters: initialAudioParameters); + } else { + return Center( + child: Text( + "Permissions for Camera and Microphone are required")); + } + } + }); + } + + Future _requestPermission(List permissions) async { + final statuses = await permissions.request(); + + var numOfPermissionsGranted = 0; + statuses.forEach((permission, status) { + if (status == PermissionStatus.granted) { + print('$permission permission Granted'); + numOfPermissionsGranted++; + } else if (status == PermissionStatus.denied) { + print('$permission permission denied'); + } else if (status == PermissionStatus.permanentlyDenied) { + print('$permission permission Permanently Denied'); + } + }); + if (numOfPermissionsGranted >= permissions.length) { + return true; + } else { + return false; + } } } diff --git a/example/lib/settings_screen.dart b/example/lib/settings_screen.dart new file mode 100644 index 0000000..86b2f80 --- /dev/null +++ b/example/lib/settings_screen.dart @@ -0,0 +1,315 @@ +import 'package:apivideo_live_stream_example/types/channel.dart'; +import 'package:flutter/material.dart'; +import 'package:settings_ui/settings_ui.dart'; + +import 'types/params.dart'; +import 'types/resolution.dart'; +import 'types/sample_rate.dart'; + +class SettingsScreen extends StatefulWidget { + const SettingsScreen({Key? key, required this.params}) : super(key: key); + final Params params; + + @override + _SettingsScreenState createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + int resultAlert = -1; + + @override + void initState() { + super.initState(); + } + + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Settings'), + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + Navigator.pop( + context, + ); + }), + ), + body: Container( + width: MediaQuery.of(context).size.width, + child: SettingsList( + sections: [ + SettingsSection( + title: Text('Video'), + tiles: [ + SettingsTile( + title: Text('Resolution'), + value: Text(widget.params.getResolutionToString()), + onPressed: (BuildContext context) { + showDialog( + context: context, + builder: (context) { + return PickerScreen( + title: "Pick a resolution", + initialValue: widget.params.video.resolution, + values: getResolutionsMap()); + }).then((value) { + if (value != null) { + setState(() { + widget.params.video.resolution = value; + }); + } + }); + }, + ), + SettingsTile( + title: Text('Framerate'), + value: Text(widget.params.video.fps.toString()), + onPressed: (BuildContext context) { + showDialog( + context: context, + builder: (context) { + return PickerScreen( + title: "Pick a frame rate", + initialValue: + widget.params.video.fps.toString(), + values: fpsList.toMap()); + }).then((value) { + if (value != null) { + setState(() { + widget.params.video.fps = value; + }); + } + }); + }, + ), + CustomSettingsTile( + child: Container( + child: Column( + children: [ + SettingsTile( + title: Text('Bitrate'), + ), + Row( + children: [ + Slider( + value: (widget.params.video.bitrate / 1024) + .toDouble(), + onChanged: (newValue) { + setState(() { + widget.params.video.bitrate = + (newValue.roundToDouble() * 1024) + .toInt(); + }); + }, + min: 500, + max: 10000, + ), + Text('${widget.params.video.bitrate}') + ], + ) + ], + ), + ), + ), + ], + ), + SettingsSection( + title: Text('Audio'), + tiles: [ + SettingsTile( + title: Text("Number of channels"), + value: Text(widget.params.getChannelToString()), + onPressed: (BuildContext context) { + showDialog( + context: context, + builder: (context) { + return PickerScreen( + title: "Pick the number of channels", + initialValue: + widget.params.getChannelToString(), + values: getChannelsMap()); + }).then((value) { + if (value != null) { + setState(() { + widget.params.audio.channel = value; + }); + } + }); + }, + ), + SettingsTile( + title: Text('Bitrate'), + value: Text(widget.params.getBitrateToString()), + onPressed: (BuildContext context) { + showDialog( + context: context, + builder: (context) { + return PickerScreen( + title: "Pick a bitrate", + initialValue: + widget.params.getChannelToString(), + values: audioBitrateList.toMap( + valueTransformation: (int e) => + bitrateToPrettyString(e))); + }).then((value) { + if (value != null) { + setState(() { + widget.params.audio.bitrate = value; + }); + } + }); + }, + ), + SettingsTile( + title: Text('Sample rate'), + value: Text(widget.params.getSampleRateToString()), + onPressed: (BuildContext context) { + showDialog( + context: context, + builder: (context) { + return PickerScreen( + title: "Pick a sample rate", + initialValue: + widget.params.getSampleRateToString(), + values: getSampleRatesMap()); + }).then((value) { + if (value != null) { + setState(() { + widget.params.audio.sampleRate = value; + }); + } + }); + }, + ), + SettingsTile.switchTile( + title: Text('Enable echo canceler'), + initialValue: widget.params.audio.enableEchoCanceler, + onToggle: (bool value) { + setState(() { + widget.params.audio.enableEchoCanceler = value; + }); + }, + ), + SettingsTile.switchTile( + title: Text('Enable noise suppressor'), + initialValue: widget.params.audio.enableNoiseSuppressor, + onToggle: (bool value) { + setState(() { + widget.params.audio.enableNoiseSuppressor = value; + }); + }, + ), + ], + ), + SettingsSection( + title: Text('Endpoint'), + tiles: [ + SettingsTile( + title: Text('RTMP endpoint'), + value: Text(widget.params.rtmpUrl), + onPressed: (BuildContext context) { + showDialog( + context: context, + builder: (context) { + return EditTextScreen( + title: "Enter RTMP endpoint URL", + initialValue: widget.params.rtmpUrl, + onChanged: (value) { + setState(() { + widget.params.rtmpUrl = value; + }); + }); + }); + }), + SettingsTile( + title: Text('Stream key'), + value: Text(widget.params.streamKey), + onPressed: (BuildContext context) { + showDialog( + context: context, + builder: (context) { + return EditTextScreen( + title: "Enter stream key", + initialValue: widget.params.streamKey, + onChanged: (value) { + setState(() { + widget.params.streamKey = value; + }); + }); + }); + }), + ], + ) + ], + )), + ); + } +} + +class PickerScreen extends StatelessWidget { + const PickerScreen({ + Key? key, + required this.title, + required this.initialValue, + required this.values, + }) : super(key: key); + + final String title; + final dynamic initialValue; + final Map values; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Settings')), + body: SettingsList( + sections: [ + SettingsSection( + title: Text(title), + tiles: values.keys.map((e) { + final value = values[e]; + + return SettingsTile( + title: Text(value!), + onPressed: (_) { + Navigator.of(context).pop(e); + }, + ); + }).toList(), + ), + ], + ), + ); + } +} + +class EditTextScreen extends StatelessWidget { + const EditTextScreen( + {Key? key, + required this.title, + required this.initialValue, + required this.onChanged}) + : super(key: key); + + final String title; + final String initialValue; + final ValueChanged? onChanged; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Settings')), + body: SettingsList( + sections: [ + SettingsSection(title: Text(title), tiles: [ + CustomSettingsTile( + child: TextField( + controller: TextEditingController(text: initialValue), + onChanged: onChanged), + ), + ]), + ], + ), + ); + } +} diff --git a/example/lib/types/channel.dart b/example/lib/types/channel.dart new file mode 100644 index 0000000..9bb787e --- /dev/null +++ b/example/lib/types/channel.dart @@ -0,0 +1,27 @@ +import 'package:apivideo_live_stream/apivideo_live_stream.dart'; + +Map getChannelsMap() { + Map map = {}; + for (final res in Channel.values) { + map[res] = res.toPrettyString(); + } + return map; +} + +extension ChannelExtension on Channel { + String toPrettyString() { + var result = ""; + switch (this) { + case Channel.mono: + result = "mono"; + break; + case Channel.stereo: + result = "stereo"; + break; + default: + result = "stereo"; + break; + } + return result; + } +} diff --git a/example/lib/types/params.dart b/example/lib/types/params.dart new file mode 100644 index 0000000..0519aba --- /dev/null +++ b/example/lib/types/params.dart @@ -0,0 +1,57 @@ +import 'dart:core'; + +import 'package:apivideo_live_stream/apivideo_live_stream.dart'; +import 'package:apivideo_live_stream_example/types/sample_rate.dart'; + +import 'channel.dart'; +import 'resolution.dart'; + +List fpsList = [24, 30]; +List audioBitrateList = [32000, 64000, 128000, 192000]; + +String defaultValueTransformation(int e) { + return "$e"; +} + +extension ListExtension on List { + Map toMap( + {Function(int e) valueTransformation = defaultValueTransformation}) { + var map = + Map.fromIterable(this, key: (e) => e, value: (e) => valueTransformation(e)); + return map; + } +} + +String bitrateToPrettyString(int bitrate) { + return "${bitrate / 1000} Kbps"; +} + +class Params { + final VideoParameters video = VideoParameters( + bitrate: 2 * 1024 * 1024, + resolution: Resolution.RESOLUTION_720, + fps: 30, + ); + final AudioParameters audio = AudioParameters( + bitrate: 128 * 1000, + channel: Channel.stereo, + sampleRate: SampleRate.kHz_48); + String rtmpUrl = "rtmp://broadcast.api.video/s/"; + String streamKey = ""; + + String getResolutionToString() { + return video.resolution.toPrettyString(); + } + + String getChannelToString() { + return audio.channel.toPrettyString(); + } + + String getBitrateToString() { + return bitrateToPrettyString(audio.bitrate); + } + + String getSampleRateToString() { + return audio.sampleRate.toPrettyString(); + } +} diff --git a/example/lib/types/resolution.dart b/example/lib/types/resolution.dart new file mode 100644 index 0000000..f6e035d --- /dev/null +++ b/example/lib/types/resolution.dart @@ -0,0 +1,39 @@ +import 'package:apivideo_live_stream/apivideo_live_stream.dart'; + +Map getResolutionsMap() { + Map map = {}; + for (final res in Resolution.values) { + map[res] = res.toPrettyString(); + } + return map; +} + +extension ResolutionExtension on Resolution { + String toPrettyString() { + var result = ""; + switch (this) { + case Resolution.RESOLUTION_240: + result = "352x240"; + break; + case Resolution.RESOLUTION_360: + result = "640x360"; + break; + case Resolution.RESOLUTION_480: + result = "858x480"; + break; + case Resolution.RESOLUTION_720: + result = "1280x720"; + break; + case Resolution.RESOLUTION_1080: + result = "1920x1080"; + break; + case Resolution.RESOLUTION_2160: + result = "3860x2160"; + break; + default: + result = "1280x720"; + break; + } + return result; + } +} diff --git a/example/lib/types/sample_rate.dart b/example/lib/types/sample_rate.dart new file mode 100644 index 0000000..04a8411 --- /dev/null +++ b/example/lib/types/sample_rate.dart @@ -0,0 +1,36 @@ +import 'package:apivideo_live_stream/apivideo_live_stream.dart'; + +Map getSampleRatesMap() { + Map map = {}; + for (final res in SampleRate.values) { + map[res] = res.toPrettyString(); + } + return map; +} + +extension SampleRateExtension on SampleRate { + String toPrettyString() { + var result = ""; + switch (this) { + case SampleRate.kHz_8: + result = "8 kHz"; + break; + case SampleRate.kHz_16: + result = "16 kHz"; + break; + case SampleRate.kHz_32: + result = "32 kHz"; + break; + case SampleRate.kHz_44_1: + result = "44.1 kHz"; + break; + case SampleRate.kHz_48: + result = "48 kHz"; + break; + default: + result = "32 kHz"; + break; + } + return result; + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock deleted file mode 100644 index b5475ce..0000000 --- a/example/pubspec.lock +++ /dev/null @@ -1,236 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - apivideolivestream: - dependency: "direct main" - description: - path: ".." - relative: true - source: path - version: "0.0.1" - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.8.1" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.15.0" - cross_file: - dependency: transitive - description: - name: cross_file - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.1+5" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.3" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.3" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - http: - dependency: transitive - description: - name: http - url: "https://pub.dartlang.org" - source: hosted - version: "0.13.3" - http_parser: - dependency: transitive - description: - name: http_parser - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.0" - image_picker: - dependency: "direct main" - description: - name: image_picker - url: "https://pub.dartlang.org" - source: hosted - version: "0.8.4+1" - image_picker_for_web: - dependency: transitive - description: - name: image_picker_for_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.3" - image_picker_platform_interface: - dependency: transitive - description: - name: image_picker_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.4.1" - js: - dependency: transitive - description: - name: js - url: "https://pub.dartlang.org" - source: hosted - version: "0.6.3" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.10" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.11.1" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.1" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.2" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" -sdks: - dart: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 1e49b54..5b7d22f 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,5 +1,5 @@ -name: apivideolivestream_example -description: Demonstrates how to use the apivideolivestream plugin. +name: apivideo_live_stream_example +description: Demonstrates how to use the apivideo_live_stream plugin. # The following line prevents the package from being accidentally published to # pub.dev using `pub publish`. This is preferred for private packages. @@ -13,10 +13,11 @@ dependencies: sdk: flutter image_picker: ^0.8.3+2 + settings_ui: ^2.0.1 - apivideolivestream: + apivideo_live_stream: # When depending on this package from a real application you should use: - # apivideolivestream: ^x.y.z + # apivideo_live_stream: ^x.y.z # See https://dart.dev/tools/pub/dependencies#version-constraints # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. @@ -25,6 +26,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 + permission_handler: ^8.3.0 dev_dependencies: flutter_test: diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart deleted file mode 100644 index 462ae06..0000000 --- a/example/test/widget_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:apivideolivestream_example/main.dart'; - -void main() { - testWidgets('Verify Platform version', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); - - // Verify that platform version is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => widget is Text && - widget.data!.startsWith('Running on:'), - ), - findsOneWidget, - ); - }); -} diff --git a/ios/Classes/ApiVideoLiveStreamPlugin.h b/ios/Classes/ApiVideoLiveStreamPlugin.h new file mode 100644 index 0000000..3051cde --- /dev/null +++ b/ios/Classes/ApiVideoLiveStreamPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface ApiVideoLiveStreamPlugin : NSObject +@end diff --git a/ios/Classes/ApivideolivestreamPlugin.m b/ios/Classes/ApiVideoLiveStreamPlugin.m similarity index 51% rename from ios/Classes/ApivideolivestreamPlugin.m rename to ios/Classes/ApiVideoLiveStreamPlugin.m index a9fb4fd..c0b0b9c 100644 --- a/ios/Classes/ApivideolivestreamPlugin.m +++ b/ios/Classes/ApiVideoLiveStreamPlugin.m @@ -1,15 +1,15 @@ -#import "ApivideolivestreamPlugin.h" -#if __has_include() -#import +#import "ApiVideoLiveStreamPlugin.h" +#if __has_include() +#import #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 "apivideolivestream-Swift.h" +#import "apivideo_live_stream-Swift.h" #endif -@implementation ApivideolivestreamPlugin +@implementation ApiVideoLiveStreamPlugin + (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftApivideolivestreamPlugin registerWithRegistrar:registrar]; + [SwiftApiVideoLiveStreamPlugin registerWithRegistrar:registrar]; } @end diff --git a/ios/Classes/ApivideolivestreamPlugin.h b/ios/Classes/ApivideolivestreamPlugin.h deleted file mode 100644 index f48d578..0000000 --- a/ios/Classes/ApivideolivestreamPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface ApivideolivestreamPlugin : NSObject -@end diff --git a/ios/Classes/SwiftApiVideoLiveStreamPlugin.swift b/ios/Classes/SwiftApiVideoLiveStreamPlugin.swift new file mode 100644 index 0000000..aacc342 --- /dev/null +++ b/ios/Classes/SwiftApiVideoLiveStreamPlugin.swift @@ -0,0 +1,242 @@ +import Flutter +import UIKit +import LiveStreamIos +import AVFoundation +import Network + +public class SwiftApiVideoLiveStreamPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "video.api.livestream/controller", binaryMessenger: registrar.messenger()) + + let factory = LiveStreamViewFactory(messenger: registrar.messenger(), channel: channel) + registrar.register(factory, withId: "") + + let instance = SwiftApiVideoLiveStreamPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + result("iOS " + UIDevice.current.systemVersion) + } +} + +class LiveStreamViewFactory: NSObject, FlutterPlatformViewFactory { + private let messenger: FlutterBinaryMessenger + private let channel: FlutterMethodChannel + + init(messenger: FlutterBinaryMessenger, channel: FlutterMethodChannel) { + self.messenger = messenger + self.channel = channel + super.init() + } + + func create( + withFrame frame: CGRect, + viewIdentifier viewId: Int64, + arguments args: Any? + ) -> FlutterPlatformView { + return LiveStreamNativeView( + frame: frame, + viewIdentifier: viewId, + arguments: args, + binaryMessenger: messenger, + channel: channel + ) + } +} + +class LiveStreamNativeView: NSObject, FlutterPlatformView { + private let liveStreamView: LiveStreamView + private let channel: FlutterMethodChannel + + init( + frame: CGRect, + viewIdentifier viewId: Int64, + arguments args: Any?, + binaryMessenger messenger: FlutterBinaryMessenger?, + channel: FlutterMethodChannel + ) { + liveStreamView = LiveStreamView(frame: frame, channel: channel) + self.channel = channel + super.init() + } + + func view() -> UIView { + return liveStreamView + } + + func handlerMethodCall(_ call: FlutterMethodCall, _ result: FlutterResult) { + switch call.method { + case "startStreaming": + if let args = call.arguments as? Dictionary, + let streamKey = args["streamKey"] as? String, + let url = args["url"] as? String { + liveStreamView.startStreaming(streamKey: streamKey, url: url) + } + break + case "stopStreaming": + liveStreamView.stopStreaming() + break + case "switchCamera": + if(liveStreamView.videoCamera == "back"){ + liveStreamView.videoCamera = "front" + }else{ + liveStreamView.videoCamera = "back" + } + break + case "setVideoParameters": + if let args = call.arguments as? Dictionary, + let bitrate = args["bitrate"] as? Double, + let resolution = args["resolution"] as? String, + let fps = args["fps"] as? Double { + liveStreamView.videoBitrate = bitrate + liveStreamView.videoResolution = resolution + liveStreamView.videoFps = fps + } + break + case "setAudioParameters": + if let args = call.arguments as? Dictionary, + let bitrate = args["bitrate"] as? Int { + liveStreamView.audioBitrate = bitrate + } + break + + case "toggleMute": + liveStreamView.audioMuted = !liveStreamView.audioMuted + break + default: + break + } + } +} + +extension String { + func toResolution() -> ApiVideoLiveStream.Resolutions{ + switch self { + case "240p": + return ApiVideoLiveStream.Resolutions.RESOLUTION_240 + case "360p": + return ApiVideoLiveStream.Resolutions.RESOLUTION_360 + case "480p": + return ApiVideoLiveStream.Resolutions.RESOLUTION_480 + case "720p": + return ApiVideoLiveStream.Resolutions.RESOLUTION_720 + case "1080p": + return ApiVideoLiveStream.Resolutions.RESOLUTION_1080 + case "2160p": + return ApiVideoLiveStream.Resolutions.RESOLUTION_2160 + default: + return ApiVideoLiveStream.Resolutions.RESOLUTION_720 + } + } + +} + +class LiveStreamView: UIView{ + private var liveStream: ApiVideoLiveStream? + private let channel: FlutterMethodChannel + public init(frame: CGRect, channel: FlutterMethodChannel) { + self.channel = channel + super.init(frame: frame) + self.liveStream = ApiVideoLiveStream(view: self) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc var videoFps: Double = 30 { + didSet { + if(videoFps == Double(liveStream!.videoFps)){ + return + } + liveStream?.videoFps = videoFps + } + } + + @objc var videoResolution: String = "720p" { + didSet { + let newResolution = videoResolution.toResolution() + if(newResolution == liveStream!.videoResolution){ + return + } + liveStream?.videoResolution = newResolution + } + } + + @objc var videoBitrate: Double = -1 { + didSet { + } + } + + @objc var videoCamera: String = "back" { + didSet { + var value : AVCaptureDevice.Position + switch videoCamera { + case "back": + value = AVCaptureDevice.Position.back + case "front": + value = AVCaptureDevice.Position.front + default: + value = AVCaptureDevice.Position.back + } + if(value == liveStream!.videoCamera){ + return + } + liveStream?.videoCamera = value + } + } + + @objc var videoOrientation: String = "landscape" { + didSet { + var value : ApiVideoLiveStream.Orientation + switch videoOrientation { + case "landscape": + value = ApiVideoLiveStream.Orientation.landscape + case "portrait": + value = ApiVideoLiveStream.Orientation.portrait + default: + value = ApiVideoLiveStream.Orientation.landscape + } + if(value == liveStream!.videoOrientation){ + return + } + liveStream?.videoOrientation = value + } + } + + @objc var audioMuted: Bool = false { + didSet { + if(audioMuted == liveStream!.audioMuted){ + return + } + liveStream?.audioMuted = audioMuted + } + } + + @objc var audioBitrate: Int = -1 { + didSet { + if(audioBitrate == liveStream!.audioBitrate){ + return + } + liveStream?.audioBitrate = audioBitrate + } + } + + @objc func startStreaming(streamKey: String, url: String) { + liveStream?.onConnectionSuccess = {() in + self.channel.invokeMethod("onConnectionSuccess", arguments: nil) + } + liveStream?.onConnectionFailed = {(code) in + self.channel.invokeMethod("onConnectionFailed", arguments: code) + } + liveStream?.onDisconnect = {() in + self.channel.invokeMethod("onDisconnect", arguments: nil) + } + liveStream?.startLiveStreamFlux(liveStreamKey: streamKey, rtmpServerUrl: url) + } + + @objc func stopStreaming() { + liveStream?.stopLiveStreamFlux() + } +} diff --git a/ios/Classes/SwiftApivideolivestreamPlugin.swift b/ios/Classes/SwiftApivideolivestreamPlugin.swift deleted file mode 100644 index a944395..0000000 --- a/ios/Classes/SwiftApivideolivestreamPlugin.swift +++ /dev/null @@ -1,285 +0,0 @@ -import Flutter -import UIKit -import LiveStreamIos -import AVFoundation - -public class SwiftApivideolivestreamPlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "apivideolivestream", binaryMessenger: registrar.messenger()) - - let factory = LiveStreamViewFactory(messenger: registrar.messenger()) - registrar.register(factory, withId: "") - - let instance = SwiftApivideolivestreamPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - //print("call method : \(call.method)") - result("iOS " + UIDevice.current.systemVersion) - } -} - -class LiveStreamViewFactory: NSObject, FlutterPlatformViewFactory { - private var messenger: FlutterBinaryMessenger - - init(messenger: FlutterBinaryMessenger) { - self.messenger = messenger - super.init() - } - - func create( - withFrame frame: CGRect, - viewIdentifier viewId: Int64, - arguments args: Any? - ) -> FlutterPlatformView { - return LiveStreamNativeView( - frame: frame, - viewIdentifier: viewId, - arguments: args, - binaryMessenger: messenger - ) - } -} - -class LiveStreamNativeView: NSObject, FlutterPlatformView { - private var _view: LiveStreamView - - init( - frame: CGRect, - viewIdentifier viewId: Int64, - arguments args: Any?, - binaryMessenger messenger: FlutterBinaryMessenger? - ) { - _view = LiveStreamView() - super.init() - - let channelFirstConnection = FlutterMethodChannel(name: "apivideolivestream_\(viewId)", binaryMessenger: messenger!) - channelFirstConnection.setMethodCallHandler { [weak self] (call, result) -> Void in - self?.handlerMethodCall(call, result) - } - - // iOS views can be created here - //createNativeView(view: _view) - } - - func view() -> UIView { - return _view - } - - func handlerMethodCall(_ call: FlutterMethodCall, _ result: FlutterResult) { - switch call.method { - case "getPlatformVersion": - print("getPlatformVersion") - result("iOS " + UIDevice.current.systemVersion) - break - case "startStreaming": - print("start STREAMING") - _view.startStreaming() - break - case "stopStreaming": - print("stop STREAMING") - _view.stopStreaming() - break - case "switchCamera": - if(_view.videoCamera == "back"){ - _view.videoCamera = "front" - }else{ - _view.videoCamera = "back" - } - break - case "setLivestreamKey": - let key = call.arguments as! String - print("key: \(key)") - _view.liveStreamKey = key - print("view key: \(_view.liveStreamKey)") - case "setParam": - let str = call.arguments as! String - print("Data: \(str)") - let data = str.data(using: .utf8) - do { - let param = try JSONDecoder().decode(Parameters.self, from: data!) - _view.liveStreamKey = param.liveStreamKey - _view.rtmpServerUrl = param.rtmpServerUrl - _view.videoFps = param.videoFps - _view.videoResolution = param.videoResolution - _view.videoBitrate = param.videoBitrate - _view.videoCamera = param.videoCamera - _view.videoOrientation = param.videoOrientation - _view.audioMuted = param.audioMuted - _view.audioBitrate = param.audioBitrate - print(param) - print(param.rtmpServerUrl) - } catch let error as NSError{ - print(error) - } - - - default: - break - } - } - - func createNativeView(view _view: UIView){ - _view.backgroundColor = UIColor.blue - let nativeLabel = UILabel() - nativeLabel.text = "Native text from iOS" - nativeLabel.textColor = UIColor.white - nativeLabel.textAlignment = .center - nativeLabel.frame = CGRect(x: 0, y: 0, width: 180, height: 48.0) - _view.addSubview(nativeLabel) - } -} - -class LiveStreamView: UIView{ - private var apiVideo: ApiVideoLiveStream? - public override init(frame: CGRect) { - super.init(frame: frame) - apiVideo = ApiVideoLiveStream(view: self) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func getResolutionFromString(resolutionString: String) -> ApiVideoLiveStream.Resolutions{ - switch resolutionString { - case "240p": - return ApiVideoLiveStream.Resolutions.RESOLUTION_240 - case "360p": - return ApiVideoLiveStream.Resolutions.RESOLUTION_360 - case "480p": - return ApiVideoLiveStream.Resolutions.RESOLUTION_480 - case "720p": - return ApiVideoLiveStream.Resolutions.RESOLUTION_720 - case "1080p": - return ApiVideoLiveStream.Resolutions.RESOLUTION_1080 - case "2160p": - return ApiVideoLiveStream.Resolutions.RESOLUTION_2160 - default: - return ApiVideoLiveStream.Resolutions.RESOLUTION_720 - } - } - - @objc override func didMoveToWindow() { - super.didMoveToWindow() - } - - @objc var liveStreamKey: String = "" { - didSet { - } - } - - @objc var rtmpServerUrl: String? { - didSet { - } - } - - @objc var videoFps: Double = 30 { - didSet { - if(videoFps == Double(apiVideo!.videoFps)){ - return - } - apiVideo?.videoFps = videoFps - } - } - - @objc var videoResolution: String = "720p" { - didSet { - let newResolution = getResolutionFromString(resolutionString: videoResolution) - if(newResolution == apiVideo!.videoResolution){ - return - } - apiVideo?.videoResolution = newResolution - } - } - - @objc var videoBitrate: Double = -1 { - didSet { - } - } - - @objc var videoCamera: String = "back" { - didSet { - var value : AVCaptureDevice.Position - switch videoCamera { - case "back": - value = AVCaptureDevice.Position.back - case "front": - value = AVCaptureDevice.Position.front - default: - value = AVCaptureDevice.Position.back - } - if(value == apiVideo?.videoCamera){ - return - } - apiVideo?.videoCamera = value - - } - } - - @objc var videoOrientation: String = "landscape" { - didSet { - var value : ApiVideoLiveStream.Orientation - switch videoOrientation { - case "landscape": - value = ApiVideoLiveStream.Orientation.landscape - case "portrait": - value = ApiVideoLiveStream.Orientation.portrait - default: - value = ApiVideoLiveStream.Orientation.landscape - } - if(value == apiVideo?.videoOrientation){ - return - } - apiVideo?.videoOrientation = value - - } - } - - @objc var audioMuted: Bool = false { - didSet { - if(audioMuted == apiVideo!.audioMuted){ - return - } - apiVideo?.audioMuted = audioMuted - } - } - - @objc var audioBitrate: Double = -1 { - didSet { - } - } - - @objc func startStreaming() { - apiVideo!.startLiveStreamFlux(liveStreamKey: self.liveStreamKey, rtmpServerUrl: self.rtmpServerUrl) - } - - @objc func stopStreaming() { - apiVideo!.stopLiveStreamFlux() - } -} - -struct Parameters: Codable { - let liveStreamKey: String - let rtmpServerUrl: String - let videoFps: Double - let videoResolution: String - let videoBitrate: Double - let videoCamera: String - let videoOrientation: String - let audioMuted: Bool - let audioBitrate: Double - - private enum CodingKeys: String, CodingKey { - case liveStreamKey = "liveStreamKey" - case videoFps = "videoFps" - case videoBitrate = "videoBitrate" - case videoOrientation = "videoOrientation" - case audioBitrate = "audioBitrate" - case rtmpServerUrl = "rtmpServerUrl" - case videoResolution = "videoResolution" - case videoCamera = "videoCamera" - case audioMuted = "audioMuted" - } -} diff --git a/ios/apivideolivestream.podspec b/ios/apivideo_live_stream.podspec similarity index 81% rename from ios/apivideolivestream.podspec rename to ios/apivideo_live_stream.podspec index cc98144..198b4c1 100644 --- a/ios/apivideolivestream.podspec +++ b/ios/apivideo_live_stream.podspec @@ -1,9 +1,9 @@ # # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint apivideolivestream.podspec` to validate before publishing. +# Run `pod lib lint apivideo_live_stream.podspec` to validate before publishing. # Pod::Spec.new do |s| - s.name = 'apivideolivestream' + s.name = 'apivideo_live_stream' s.version = '0.0.1' s.summary = 'A new flutter plugin project.' s.description = <<-DESC @@ -15,7 +15,7 @@ A new flutter plugin project. s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.dependency 'LiveStreamIos', '~> 0.0.2' + s.dependency 'LiveStreamIos', '~> 0.0.4' s.platform = :ios, '8.0' # Flutter.framework does not contain a i386 slice. diff --git a/lib/apivideo_live_stream.dart b/lib/apivideo_live_stream.dart new file mode 100644 index 0000000..a2eb225 --- /dev/null +++ b/lib/apivideo_live_stream.dart @@ -0,0 +1,111 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'src/types/audio_parameters.dart'; +import 'src/types/video_parameters.dart'; + +export 'src/types.dart'; + +class LiveStreamController { + static const MethodChannel _channel = + const MethodChannel('video.api.livestream/controller'); + final Function()? onConnectionSuccess; + final Function(String)? onConnectionError; + final Function()? onDisconnection; + + LiveStreamController({ + this.onConnectionSuccess, + this.onConnectionError, + this.onDisconnection, + }) { + _channel.setMethodCallHandler(_methodCallHandler); + } + + void setVideoParameters(VideoParameters videoParameters) { + _channel.invokeMethod('setVideoParameters', videoParameters.toJson()); + } + + void setAudioParameters(AudioParameters audioParameters) { + _channel.invokeMethod('setAudioParameters', audioParameters.toJson()); + } + + Future startStreaming({required String streamKey, + String url = "rtmp://broadcast.api.video/s/"}) { + return _channel.invokeMethod('startStreaming', { + 'streamKey': streamKey, + 'url': url, + }); + } + + void stopStreaming() { + _channel.invokeMethod('stopStreaming'); + } + + void switchCamera() { + _channel.invokeMethod('switchCamera'); + } + + void toggleMute() { + _channel.invokeMethod('toggleMute'); + } + + Future _methodCallHandler(MethodCall call) async { + switch (call.method) { + case "onConnectionSuccess": + if (onConnectionSuccess != null) { + onConnectionSuccess!(); + } + break; + case "onConnectionFailed": + if (onConnectionError != null) { + String error = call.arguments; + onConnectionError!("$error"); + } + break; + case "onDisconnect": + if (onDisconnection != null) { + onDisconnection!(); + } + break; + } + } +} + +class CameraPreview extends StatelessWidget { + final LiveStreamController controller; + final VideoParameters initialVideoParameters; + final AudioParameters initialAudioParameters; + + const CameraPreview({required this.controller, + required this.initialAudioParameters, + required this.initialVideoParameters}); + + @override + Widget build(BuildContext context) { + final String viewType = ''; + final Map creationParams = { + "audioParameters": initialAudioParameters.toJson(), + "videoParameters": initialVideoParameters.toJson() + }; + + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return AndroidView( + viewType: viewType, + creationParamsCodec: const StandardMessageCodec(), + creationParams: creationParams); + + case TargetPlatform.iOS: + return UiKitView( + viewType: viewType, + layoutDirection: TextDirection.ltr, + creationParamsCodec: const StandardMessageCodec(), + creationParams: creationParams); + default: + throw UnsupportedError("Unsupported platform view"); + } + } +} diff --git a/lib/apivideolivestream.dart b/lib/apivideolivestream.dart deleted file mode 100644 index 2b36f41..0000000 --- a/lib/apivideolivestream.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -class Apivideolivestream { - static const MethodChannel _channel = - const MethodChannel('apivideolivestream_0'); - - static Future get platformVersion async { - final String? version = await _channel.invokeMethod('getPlatformVersion'); - return version; - } - - static Future startStream() async { - print("start stream called"); - await _channel.invokeMethod('startStreaming'); - } - - static Future stopStream() async { - print("stop stream called"); - await _channel.invokeMethod('stopStreaming'); - } - - static void switchCamera() async { - await _channel.invokeMethod('switchCamera'); - } - - static void changeMute() async { - await _channel.invokeMethod('changeMute'); - } -} - -class LiveStreamPreview extends StatefulWidget { - final Apivideolivestream controller; - final String liveStreamKey; - final String? rtmpServerUrl; - final double? videoFps; - final String? videoResolution; - final double? videoBitrate; - final String? videoCamera; - final String? videoOrientation; - final bool? audioMuted; - final double? audioBitrate; - - const LiveStreamPreview({ - required this.controller, - required this.liveStreamKey, - this.rtmpServerUrl, - this.videoFps, - this.videoResolution, - this.videoBitrate, - this.videoCamera, - this.videoOrientation, - this.audioMuted, - this.audioBitrate, - }); - - @override - _LiveStreamPreviewState createState() => _LiveStreamPreviewState(); -} - -class _LiveStreamPreviewState extends State { - late MethodChannel _channel; - late Apivideolivestream _controller; - Set _updateMap = {}; - - createParams() { - var param = {}; - param["liveStreamKey"] = widget.liveStreamKey; - param["rtmpServerUrl"] = widget.rtmpServerUrl ?? 'rtmp://broadcast.api.video/s'; - param["videoFps"] = widget.videoFps ?? 30; - param["videoResolution"] = widget.videoResolution ?? '720p'; - param["videoBitrate"] = widget.videoBitrate ?? -1; - param["videoCamera"] = widget.videoCamera ?? "back"; - param["videoOrientation"] = widget.videoOrientation ?? 'landscape'; - param["audioMuted"] = widget.audioMuted ?? false; - param["audioBitrate"] = widget.audioBitrate ?? -1; - return param; - } - - @override - void initState() { - _controller = widget.controller; - if (widget.liveStreamKey.isNotEmpty) { - Future.delayed(const Duration(milliseconds: 300)).then((value) { - _channel.invokeMethod('setLivestreamKey', widget.liveStreamKey); - }); - } - Future.delayed(const Duration(milliseconds: 300)).then((value) { - _channel.invokeMethod('setParam', json.encode(createParams())); - }); - super.initState(); - } - - @override - Widget build(BuildContext context) { - final String viewType = ''; - // Pass parameters to the platform side. - - switch (defaultTargetPlatform) { - case TargetPlatform.android: - return SizedBox( - height: 400, - child: PlatformViewLink( - viewType: viewType, - surfaceFactory: (BuildContext context, dynamic controller) { - return AndroidViewSurface( - controller: controller, - gestureRecognizers: const < - Factory>{}, - hitTestBehavior: PlatformViewHitTestBehavior.opaque, - ); - }, - onCreatePlatformView: (PlatformViewCreationParams params) { - return PlatformViewsService.initSurfaceAndroidView( - id: params.id, - viewType: viewType, - layoutDirection: TextDirection.ltr, - creationParams: createParams(), - creationParamsCodec: StandardMessageCodec(), - ) - ..addOnPlatformViewCreatedListener((id) { - _channel = MethodChannel('apivideolivestream_$id'); - //_channel.setMethodCallHandler(_handlerCall); - }) - ..create(); - }, - )); - - case TargetPlatform.iOS: - return SizedBox( - height: 400, - child: UiKitView( - viewType: viewType, - layoutDirection: TextDirection.ltr, - creationParams: createParams(), - onPlatformViewCreated: (viewId) { - _channel = MethodChannel('apivideolivestream_$viewId'); - }, - creationParamsCodec: const StandardMessageCodec(), - ), - ); - default: - throw UnsupportedError("Unsupported platform view"); - } - } -} diff --git a/lib/src/types.dart b/lib/src/types.dart new file mode 100644 index 0000000..4910699 --- /dev/null +++ b/lib/src/types.dart @@ -0,0 +1,5 @@ +export 'types/audio_parameters.dart'; +export 'types/channel.dart'; +export 'types/resolution.dart'; +export 'types/sample_rate.dart'; +export 'types/video_parameters.dart'; \ No newline at end of file diff --git a/lib/src/types/audio_parameters.dart b/lib/src/types/audio_parameters.dart new file mode 100644 index 0000000..27371b4 --- /dev/null +++ b/lib/src/types/audio_parameters.dart @@ -0,0 +1,27 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'channel.dart'; +import 'sample_rate.dart'; + +part 'audio_parameters.g.dart'; + +@JsonSerializable() +class AudioParameters { + int bitrate; + Channel channel; + SampleRate sampleRate; + bool enableEchoCanceler; + bool enableNoiseSuppressor; + + AudioParameters( + {required this.bitrate, + required this.channel, + required this.sampleRate, + this.enableEchoCanceler = true, + this.enableNoiseSuppressor = true}); + + factory AudioParameters.fromJson(Map json) => + _$AudioParametersFromJson(json); + + Map toJson() => _$AudioParametersToJson(this); +} diff --git a/lib/src/types/audio_parameters.g.dart b/lib/src/types/audio_parameters.g.dart new file mode 100644 index 0000000..4ada126 --- /dev/null +++ b/lib/src/types/audio_parameters.g.dart @@ -0,0 +1,38 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'audio_parameters.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AudioParameters _$AudioParametersFromJson(Map json) => + AudioParameters( + bitrate: json['bitrate'] as int, + channel: $enumDecode(_$ChannelEnumMap, json['channel']), + sampleRate: $enumDecode(_$SampleRateEnumMap, json['sampleRate']), + enableEchoCanceler: json['enableEchoCanceler'] as bool? ?? true, + enableNoiseSuppressor: json['enableNoiseSuppressor'] as bool? ?? true, + ); + +Map _$AudioParametersToJson(AudioParameters instance) => + { + 'bitrate': instance.bitrate, + 'channel': _$ChannelEnumMap[instance.channel], + 'sampleRate': _$SampleRateEnumMap[instance.sampleRate], + 'enableEchoCanceler': instance.enableEchoCanceler, + 'enableNoiseSuppressor': instance.enableNoiseSuppressor, + }; + +const _$ChannelEnumMap = { + Channel.stereo: 'stereo', + Channel.mono: 'mono', +}; + +const _$SampleRateEnumMap = { + SampleRate.kHz_8: 8000, + SampleRate.kHz_16: 16000, + SampleRate.kHz_32: 32000, + SampleRate.kHz_44_1: 44100, + SampleRate.kHz_48: 48000, +}; diff --git a/lib/src/types/channel.dart b/lib/src/types/channel.dart new file mode 100644 index 0000000..a2da8d3 --- /dev/null +++ b/lib/src/types/channel.dart @@ -0,0 +1,8 @@ +import 'package:json_annotation/json_annotation.dart'; + +enum Channel { + @JsonValue("stereo") + stereo, + @JsonValue("mono") + mono, +} diff --git a/lib/src/types/resolution.dart b/lib/src/types/resolution.dart new file mode 100644 index 0000000..4eab113 --- /dev/null +++ b/lib/src/types/resolution.dart @@ -0,0 +1,16 @@ +import 'package:json_annotation/json_annotation.dart'; + +enum Resolution { + @JsonValue("240p") + RESOLUTION_240, + @JsonValue("360p") + RESOLUTION_360, + @JsonValue("480p") + RESOLUTION_480, + @JsonValue("720p") + RESOLUTION_720, + @JsonValue("1080") + RESOLUTION_1080, + @JsonValue("2160p") + RESOLUTION_2160, +} diff --git a/lib/src/types/sample_rate.dart b/lib/src/types/sample_rate.dart new file mode 100644 index 0000000..f36c801 --- /dev/null +++ b/lib/src/types/sample_rate.dart @@ -0,0 +1,14 @@ +import 'package:json_annotation/json_annotation.dart'; + +enum SampleRate { + @JsonValue(8000) + kHz_8, + @JsonValue(16000) + kHz_16, + @JsonValue(32000) + kHz_32, + @JsonValue(44100) + kHz_44_1, + @JsonValue(48000) + kHz_48, +} diff --git a/lib/src/types/video_parameters.dart b/lib/src/types/video_parameters.dart new file mode 100644 index 0000000..4e07752 --- /dev/null +++ b/lib/src/types/video_parameters.dart @@ -0,0 +1,20 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'resolution.dart'; + +part 'video_parameters.g.dart'; + +@JsonSerializable() +class VideoParameters { + int bitrate; + Resolution resolution; + int fps; + + VideoParameters( + {required this.bitrate, required this.resolution, required this.fps}); + + factory VideoParameters.fromJson(Map json) => + _$VideoParametersFromJson(json); + + Map toJson() => _$VideoParametersToJson(this); +} diff --git a/lib/src/types/video_parameters.g.dart b/lib/src/types/video_parameters.g.dart new file mode 100644 index 0000000..4b1593b --- /dev/null +++ b/lib/src/types/video_parameters.g.dart @@ -0,0 +1,30 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'video_parameters.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +VideoParameters _$VideoParametersFromJson(Map json) => + VideoParameters( + bitrate: json['bitrate'] as int, + resolution: $enumDecode(_$ResolutionEnumMap, json['resolution']), + fps: json['fps'] as int, + ); + +Map _$VideoParametersToJson(VideoParameters instance) => + { + 'bitrate': instance.bitrate, + 'resolution': _$ResolutionEnumMap[instance.resolution], + 'fps': instance.fps, + }; + +const _$ResolutionEnumMap = { + Resolution.RESOLUTION_240: '240p', + Resolution.RESOLUTION_360: '360p', + Resolution.RESOLUTION_480: '480p', + Resolution.RESOLUTION_720: '720p', + Resolution.RESOLUTION_1080: '1080', + Resolution.RESOLUTION_2160: '2160p', +}; diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index 4935c14..0000000 --- a/pubspec.lock +++ /dev/null @@ -1,147 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.8.1" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.15.0" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.10" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.1" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.2" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" -sdks: - dart: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" diff --git a/pubspec.yaml b/pubspec.yaml index bf536ff..63ad48c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,10 @@ -name: apivideolivestream -description: A new flutter plugin project. -version: 0.0.1 -homepage: +name: apivideo_live_stream +description: An easy way to broadcast livestream on api.video platform as well as other RTMP platforms. +version: 0.1.0 +repository: https://github.com/apivideo/api.video-flutter-live-stream +issue_tracker: https://github.com/apivideo/api.video-flutter-live-stream/issues +homepage: https://api.video +documentation: https://docs.api.video environment: sdk: ">=2.12.0 <3.0.0" @@ -10,10 +13,14 @@ environment: dependencies: flutter: sdk: flutter + json_annotation: ^4.4.0 + dev_dependencies: flutter_test: sdk: flutter + build_runner: ^2.1.7 + json_serializable: ^6.1.3 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -27,10 +34,10 @@ flutter: plugin: platforms: android: - package: video.api.eco.flt.livestream.apivideolivestream - pluginClass: ApivideolivestreamPlugin + package: video.api.flutter.livestream + pluginClass: ApiVideoLiveStreamPlugin ios: - pluginClass: ApivideolivestreamPlugin + pluginClass: ApiVideoLiveStreamPlugin # To add assets to your plugin package, add an assets section, like this: # assets: diff --git a/test/apivideolivestream_test.dart b/test/apivideo_live_stream_test.dart similarity index 70% rename from test/apivideolivestream_test.dart rename to test/apivideo_live_stream_test.dart index 73988a6..1604b5d 100644 --- a/test/apivideolivestream_test.dart +++ b/test/apivideo_live_stream_test.dart @@ -1,6 +1,5 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:apivideolivestream/apivideolivestream.dart'; void main() { const MethodChannel channel = MethodChannel('apivideolivestream'); @@ -16,8 +15,4 @@ void main() { tearDown(() { channel.setMockMethodCallHandler(null); }); - - test('getPlatformVersion', () async { - expect(await Apivideolivestream.platformVersion, '42'); - }); }