diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..aa81b8a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI + +on: [push, pull_request] + +jobs: + analyze-and-test: + runs-on: ubuntu-latest + strategy: + matrix: + package: + - packages/flutter_radio_player + - packages/flutter_radio_player_platform_interface + - packages/flutter_radio_player_android + - packages/flutter_radio_player_ios + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: stable + - run: flutter pub get + working-directory: ${{ matrix.package }} + - run: flutter analyze --no-fatal-warnings + working-directory: ${{ matrix.package }} + - run: flutter test + working-directory: ${{ matrix.package }} diff --git a/CHANGELOG.md b/CHANGELOG.md index b5646d5..1f7a895 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +# 4.0.0 + +* **BREAKING**: Full rewrite with federated plugin architecture (monorepo) +* **BREAKING**: Replaced `List>` with typed `RadioSource` model +* **BREAKING**: Renamed stream getters (`getPlaybackStream()` → `isPlayingStream`, etc.) +* **BREAKING**: Renamed `prevSource()` → `previousSource()` +* **BREAKING**: `getVolume()` now returns `Future` (non-nullable) +* Added Pigeon for type-safe platform channels (replaces manual method/event channels) +* iOS: Replaced SwiftAudioEx with direct AVFoundation (no third-party deps) +* iOS: Raised minimum deployment target to 14.0 +* iOS: Implemented `playOrPause()` (was missing) +* iOS: Added artwork URL support (was asset-only) +* iOS: Fixed volume event double-emission bug +* Added `dispose()` method on both platforms +* Added CI/CD via GitHub Actions +* Removed kotlinx-serialization dependency on Android + # 3.0.2 * Added foreground title when title was provided along with artist title diff --git a/README.md b/README.md index 995abdd..d6c342e 100644 --- a/README.md +++ b/README.md @@ -2,91 +2,91 @@ # Flutter Radio Player -![Pub Version](https://img.shields.io/pub/v/flutter_radio_player?style=plastic) -![Pub Likes](https://img.shields.io/pub/likes/flutter_radio_player) -![Pub Points](https://img.shields.io/pub/points/flutter_radio_player) -![Pub Popularity](https://img.shields.io/pub/popularity/flutter_radio_player) +[![Pub Version](https://img.shields.io/pub/v/flutter_radio_player)](https://pub.dev/packages/flutter_radio_player) +[![Pub Likes](https://img.shields.io/pub/likes/flutter_radio_player)](https://pub.dev/packages/flutter_radio_player) +[![Pub Points](https://img.shields.io/pub/points/flutter_radio_player)](https://pub.dev/packages/flutter_radio_player) +[![CI](https://github.com/Sithira/FlutterRadioPlayer/actions/workflows/ci.yml/badge.svg)](https://github.com/Sithira/FlutterRadioPlayer/actions/workflows/ci.yml) -**Flutter Radio Player** is the go-to plugin for playing a single streaming URL effortlessly. With support for background music playback right out of the box, it offers seamless integration with platform-native media controls. Whether it's lock screen media controls or deeper integrations like watchOS, CarPlay, WearOS, or Android Auto, Flutter Radio Player handles it all with no extra configuration needed. +A Flutter plugin for playing streaming radio URLs with background playback, lock screen controls, and platform-native media integrations including watchOS, WearOS, CarPlay, and Android Auto. + +| | Android | iOS | +|-------------|---------|---------| +| **Support** | SDK 21+ | iOS 14+ | ## Features -- **Background Playback**: Plays audio in the background without any configuration. -- **Watch Integration**: Seamlessly integrates with WatchOS and WearOS for native watch control. -- **Automotive Systems**: Supports infotainment systems like Apple CarPlay and Android Auto. -- **Reactive by Default**: Automatically reacts to stream changes. -- **ICY/Metadata Extraction**: Extracts stream metadata if available. +- Background audio playback with no extra configuration +- Lock screen and notification media controls +- ICY/stream metadata extraction +- Multiple source queue with next/previous/jump-to navigation +- Volume control with stream updates +- watchOS, WearOS, CarPlay, and Android Auto integration ## Getting Started -### 1. Install the Player +### Installation ```bash flutter pub add flutter_radio_player ``` -### 2. Import the Library +### Usage ```dart import 'package:flutter_radio_player/flutter_radio_player.dart'; -``` -### 3. Configure the Player +final player = FlutterRadioPlayer(); -```dart -final _flutterRadioPlayerPlugin = FlutterRadioPlayer(); // Create an instance of the player -_flutterRadioPlayerPlugin.initialize( +// Initialize with sources +await player.initialize( [ - {"url": "https://s2-webradio.antenne.de/chillout?icy=https"}, - { - "title": "SunFM - Sri Lanka", - "artwork": "images/sample-cover.jpg", // Image needs to be bundled with the app - "url": "https://radio.lotustechnologieslk.net:2020/stream/sunfmgarden?icy=https", - }, - {"url": "http://stream.riverradio.com:8000/wcvofm.aac"} + const RadioSource(url: 'https://s2-webradio.antenne.de/chillout?icy=https'), + const RadioSource( + url: 'https://radio.lotustechnologieslk.net:2020/stream/sunfmgarden?icy=https', + title: 'SunFM - Sri Lanka', + artwork: 'images/sample-cover.jpg', // bundled asset + ), + const RadioSource(url: 'http://stream.riverradio.com:8000/wcvofm.aac'), ], - true, // Auto play on load + playWhenReady: true, ); ``` -Once configured, your player is ready to stream music. - -### Manipulating the Player - -You can control the player using the following methods: - -| Method | Action | -|------------------------|------------------------------------------------------------| -| `play()` | Plays the audio from the current source | -| `pause()` | Pauses the audio | -| `playOrPause()` | Toggles play/pause | -| `changeVolume()` | Adjusts the volume | -| `getVolume()` | Retrieves the current volume | -| `nextSource()` | Skips to the next source in the list (if available) | -| `previousSource()` | Goes to the previous source | -| `jumpToSourceIndex()` | Jumps to a specific index in the sources list | +### Controlling Playback -### Available Streams - -You can also listen to various streams: +```dart +await player.play(); +await player.pause(); +await player.playOrPause(); +await player.nextSource(); +await player.previousSource(); +await player.jumpToSourceAtIndex(1); +await player.setVolume(0.8); +final volume = await player.getVolume(); +await player.dispose(); +``` -| Stream | Returns | Description | -|-----------------------------------|-------------------------------------|------------------------------------------------------| -| `getIsPlayingStream()` | `Stream` | Emits playback status | -| `getNowPlayingStream()` | `Stream` | Emits metadata such as track name | -| `getDeviceVolumeChangedStream()` | `Stream` | Emits device audio level updates | +### Listening to Streams -## Platform Configuration +```dart +player.isPlayingStream.listen((bool isPlaying) { + print('Playing: $isPlaying'); +}); -### iOS +player.nowPlayingStream.listen((NowPlayingInfo info) { + print('Now playing: ${info.title}'); +}); -To enable background playback, configure background capabilities in Xcode as shown below: +player.volumeStream.listen((VolumeInfo vol) { + print('Volume: ${vol.volume}, Muted: ${vol.isMuted}'); +}); +``` -![Xcode Configuration](enabling-xcode-bg-service.png) +## Platform Setup ### Android -For Android, ensure the following permissions are added to your `AndroidManifest.xml`: +Add the following permissions to your app's `AndroidManifest.xml`: ```xml @@ -94,9 +94,91 @@ For Android, ensure the following permissions are added to your `AndroidManifest ``` -> These permissions are already included in the library. +> These permissions are already declared by the plugin. You only need to add them if your app's manifest merger requires it. + +### iOS + +Enable **Audio, AirPlay, and Picture in Picture** under your target's **Signing & Capabilities > Background Modes** in Xcode: + +![Xcode Configuration](xcode_required_capabilities.png) + +If your radio streams use plain HTTP, add the following to your `Info.plist`: + +```xml +NSAppTransportSecurity + + NSAllowsArbitraryLoads + + +``` + +## API Reference + +### Methods + +| Method | Description | +|---------------------------|----------------------------------------| +| `initialize(sources)` | Set sources and optionally auto-play | +| `play()` | Resume playback | +| `pause()` | Pause playback | +| `playOrPause()` | Toggle play/pause | +| `setVolume(double)` | Set volume (0.0 to 1.0) | +| `getVolume()` | Get current volume | +| `nextSource()` | Skip to next source | +| `previousSource()` | Skip to previous source | +| `jumpToSourceAtIndex(i)` | Jump to source at index | +| `dispose()` | Release player resources | + +### Streams + +| Stream | Type | Description | +|---------------------|-------------------------|----------------------------| +| `isPlayingStream` | `Stream` | Playback state changes | +| `nowPlayingStream` | `Stream`| Track metadata updates | +| `volumeStream` | `Stream` | Volume and mute changes | + +### Models + +```dart +const RadioSource({required String url, String? title, String? artwork}); +const NowPlayingInfo({String? title}); +const VolumeInfo({required double volume, required bool isMuted}); +``` + +## Migration from v3 + +```diff +// Sources +- player.initialize([{"url": "...", "title": "..."}], true); ++ player.initialize([const RadioSource(url: '...', title: '...')], playWhenReady: true); + +// Streams +- player.getPlaybackStream() ++ player.isPlayingStream + +- player.getNowPlayingStream() ++ player.nowPlayingStream + +- player.getDeviceVolumeChangedStream() ++ player.volumeStream + +// Methods +- player.prevSource() ++ player.previousSource() + +- player.setVolume(0.5) // unchanged ++ player.setVolume(0.5) + +// New ++ await player.dispose(); ++ await player.playOrPause(); // now works on iOS too +``` + +## Example -**Check out the [Flutter Radio Player Example](/example)** to see how to implement action methods and streams in your player. +![Example Player](example_player.png) + +See the [example app](example/) for a complete implementation. ## Support the Plugin @@ -106,5 +188,13 @@ If you find this plugin useful, show your support by: - Leaving a like on Pub - Showing some ♥️ and buying me a coffee via USDT-TR20 at this address: `TNuTkL1ZJGu2xntmtzHzSiH5YdVqUeAujr` -**Enjoy the plugin!** -Sithira ✌️ \ No newline at end of file +**Enjoy the plugin!** +Sithira ✌️ + +## Contributing + +Contributions are welcome. Please open an issue first to discuss what you would like to change. + +## License + +[MIT](LICENSE) diff --git a/android/.gitignore b/android/.gitignore deleted file mode 100644 index 161bdcd..0000000 --- a/android/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures -.cxx diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 7f93135..0000000 Binary files a/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 3fa8f86..0000000 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew deleted file mode 100755 index 1aa94a4..0000000 --- a/android/gradlew +++ /dev/null @@ -1,249 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat deleted file mode 100644 index 93e3f59..0000000 --- a/android/gradlew.bat +++ /dev/null @@ -1,92 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/android/settings.gradle b/android/settings.gradle deleted file mode 100644 index 95ed40c..0000000 --- a/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'flutter_radio_player' diff --git a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/FlutterRadioPlayerPlugin.kt b/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/FlutterRadioPlayerPlugin.kt deleted file mode 100644 index 7b298cb..0000000 --- a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/FlutterRadioPlayerPlugin.kt +++ /dev/null @@ -1,338 +0,0 @@ -package me.sithiramunasinghe.flutter.flutter_radio_player - -import android.app.Activity -import android.app.PendingIntent -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.content.res.AssetManager -import android.net.Uri -import androidx.annotation.OptIn -import androidx.media3.common.MediaItem -import androidx.media3.common.MediaMetadata -import androidx.media3.common.util.UnstableApi -import androidx.media3.common.util.Util -import androidx.media3.session.MediaController -import androidx.media3.session.SessionToken -import com.google.common.util.concurrent.MoreExecutors -import io.flutter.embedding.engine.loader.FlutterLoader -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.embedding.engine.plugins.activity.ActivityAware -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result -import kotlinx.serialization.json.Json -import me.sithiramunasinghe.flutter.flutter_radio_player.core.EventChannelSink -import me.sithiramunasinghe.flutter.flutter_radio_player.core.PlaybackService -import me.sithiramunasinghe.flutter.flutter_radio_player.data.FlutterRadioPlayerSource -import me.sithiramunasinghe.flutter.flutter_radio_player.data.NowPlayingInfo -import java.io.InputStream - -class FlutterRadioPlayerPlugin : FlutterPlugin, ActivityAware, MethodCallHandler { - - private lateinit var channel: MethodChannel - private var applicationContext: Context? = null - private var mediaController: MediaController? = null - private val queuedMethodInvokes = mutableListOf>() - - companion object { - private var isMediaControllerAvailable = false - lateinit var sessionActivity: PendingIntent - - var playBackEventSink: EventChannel.EventSink? = null - var nowPlayingEventSink: EventChannel.EventSink? = null - var playbackVolumeControl: EventChannel.EventSink? = null - private fun getSessionActivity(context: Context, activity: Activity) { - sessionActivity = PendingIntent.getActivity( - context, 0, Intent(context, activity::class.java), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - } - } - - - override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_radio_player") - channel.setMethodCallHandler(this) - applicationContext = flutterPluginBinding.applicationContext - - initEventChannels(flutterPluginBinding.binaryMessenger, EventChannelSink.getInstance()) - initializeEventSink() - - val token = SessionToken( - flutterPluginBinding.applicationContext, ComponentName( - flutterPluginBinding.applicationContext, - PlaybackService::class.java - ) - ) - - val mediaControllerFuture = MediaController.Builder(applicationContext!!, token) - .buildAsync() - - mediaControllerFuture.addListener({ - mediaController = mediaControllerFuture.get() - isMediaControllerAvailable = true - executePendingCalls() - }, MoreExecutors.directExecutor()) - } - - @OptIn(UnstableApi::class) - override fun onMethodCall(call: MethodCall, result: Result) { - if (!isMediaControllerAvailable || playBackEventSink == null) { - queuedMethodInvokes.add(Pair(call, result)) - return - } - when (call.method) { - "initialize" -> { - if (mediaController!!.isPlaying) { - playBackEventSink!!.success(true) - nowPlayingEventSink!!.success( - NowPlayingInfo(title = PlaybackService.latestMetadata?.title.toString()) - .toJson() - ) - return - } - val sources = call.argument("sources") - val playWhenReady = call.argument("playWhenReady") - val decodedSources: List = - Json.decodeFromString(sources!!) - mediaController!!.volume = 0.5F - mediaController!!.playWhenReady = playWhenReady!! - if (decodedSources.isNotEmpty()) { - mediaController!!.setMediaItems(decodedSources.map { - val mediaItemBuilder = MediaItem.Builder().setUri(it.url) - val mediaMeta = MediaMetadata.Builder() - if (it.title.isNullOrEmpty()) { - mediaMeta.setArtist(getAppName()) - } else { - mediaMeta.setTitle(it.title) - mediaMeta.setArtist(getAppName()) - } - if (!it.artwork.isNullOrEmpty()) { - if ((it.artwork!!.contains("http") || it.artwork!!.contains("https"))) { - mediaMeta.setArtworkUri(Uri.parse(it.artwork)) - } else { - mediaMeta.setArtworkData( - Util.toByteArray(getBitmapFromAssets(it.artwork)!!), - MediaMetadata.PICTURE_TYPE_FRONT_COVER - ) - } - } - mediaItemBuilder.setMediaMetadata(mediaMeta.build()) - mediaItemBuilder.build() - }) - mediaController!!.prepare() - } - } - - "playOrPause" -> { - if (mediaController!!.mediaItemCount != 0) { - if (mediaController!!.isPlaying) { - mediaController!!.pause() - return - } - mediaController!!.play() - } - } - - "play" -> { - mediaController!!.play() - } - - "pause" -> { - mediaController!!.pause() - } - - "changeVolume" -> { - val volume = call.argument("volume") - mediaController!!.volume = volume!!.toFloat() - } - - "getVolume" -> { - result.success(mediaController!!.volume) - } - - "nextSource" -> { - clearInMemoryNowPlayingInfo() - mediaController!!.seekToNextMediaItem() - } - - "prevSource" -> { - clearInMemoryNowPlayingInfo() - mediaController!!.seekToPreviousMediaItem() - } - - "sourceAtIndex" -> { - clearInMemoryNowPlayingInfo() - val index = call.argument("index") - mediaController!!.seekToDefaultPosition(index!!) - } - - else -> result.notImplemented() - } - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - println("onDetachedFromEngine") - channel.setMethodCallHandler(null) - - EventChannelSink.getInstance().playbackEventChannel = null - EventChannelSink.getInstance().nowPlayingEventChannel = null - EventChannelSink.getInstance().playbackVolumeChannel = null - - playbackVolumeControl = null - nowPlayingEventSink = null - playBackEventSink = null - mediaController!!.release() - isMediaControllerAvailable = false - } - - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - println("onAttachedToActivity") - getSessionActivity(binding.activity.applicationContext, binding.activity) - } - - override fun onDetachedFromActivityForConfigChanges() { - println("onDetachedFromActivityForConfigChanges") - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - println("onReattachedToActivityForConfigChanges") - getSessionActivity(binding.activity.applicationContext, binding.activity) - } - - override fun onDetachedFromActivity() { - println("onDetachedFromActivity") - } - - /** - * Initialize events sink and event channels - */ - private fun initializeEventSink() { - EventChannelSink.getInstance().playbackEventChannel?.setStreamHandler(object : - EventChannel.StreamHandler { - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - playBackEventSink = events - } - - override fun onCancel(arguments: Any?) { - playBackEventSink = null - } - }) - - EventChannelSink.getInstance().nowPlayingEventChannel?.setStreamHandler(object : - EventChannel.StreamHandler { - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - nowPlayingEventSink = events - executePendingCalls() - } - - override fun onCancel(arguments: Any?) { - nowPlayingEventSink = null - } - }) - - EventChannelSink.getInstance().playbackVolumeChannel?.setStreamHandler(object : - EventChannel.StreamHandler { - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - playbackVolumeControl = events - } - - override fun onCancel(arguments: Any?) { - playbackVolumeControl = null - } - }) - } - - /** - * Execute pending method calls - */ - private fun executePendingCalls() { - if (mediaController == null) { - return - } - queuedMethodInvokes.forEach { (call, result) -> - onMethodCall(call, result) - } - queuedMethodInvokes.clear() - } - - private fun clearInMemoryNowPlayingInfo() { - PlaybackService.latestMetadata = null - } - - /** - * Invoke event channels - * - * @param binaryMessenger Binary messenger - * @param eventsChannelSink Event channel sink - */ - private fun initEventChannels( - binaryMessenger: BinaryMessenger, - eventsChannelSink: EventChannelSink - ) { - val playbackEventChannel = - EventChannel( - binaryMessenger, - "flutter_radio_player/playback_status" - ) - - val nowPlayingEventChannel = EventChannel( - binaryMessenger, - "flutter_radio_player/now_playing_info" - ) - - val playbackVolumeControl = EventChannel( - binaryMessenger, - "flutter_radio_player/volume_control" - ) - - eventsChannelSink.playbackEventChannel = playbackEventChannel - eventsChannelSink.nowPlayingEventChannel = nowPlayingEventChannel - eventsChannelSink.playbackVolumeChannel = playbackVolumeControl - } - - /** - * Load album artwork as an URI for from app bundle - * - * @param assetPath bundle resource path - * @return InputStream of asset - */ - private fun getBitmapFromAssets(assetPath: String?): InputStream? { - try { - val flutterLoader = FlutterLoader() - flutterLoader.startInitialization(applicationContext!!) - flutterLoader.ensureInitializationComplete(applicationContext!!, null) - val assetLookupKey = flutterLoader.getLookupKeyForAsset(assetPath!!) - val assetManager: AssetManager = applicationContext!!.assets - return assetManager.open(assetLookupKey) -// val inputStream = assetManager.open(assetLookupKey) -// return BitmapFactory.decodeStream(inputStream). - } catch (e: Exception) { - e.printStackTrace() - return null - } - } - - /** - * Get the application name - * - * @return App name - */ - private fun getAppName(): String? { - try { - val packageManager: PackageManager = applicationContext!!.packageManager - val applicationInfo = - packageManager.getApplicationInfo(applicationContext!!.packageName, 0) - return packageManager.getApplicationLabel(applicationInfo) as String - } catch (e: PackageManager.NameNotFoundException) { - e.printStackTrace() - return null - } - } -} diff --git a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/EventChannelSink.kt b/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/EventChannelSink.kt deleted file mode 100644 index 916ee4f..0000000 --- a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/EventChannelSink.kt +++ /dev/null @@ -1,26 +0,0 @@ -package me.sithiramunasinghe.flutter.flutter_radio_player.core - -import io.flutter.plugin.common.EventChannel - - -class EventChannelSink private constructor() { - var playbackEventChannel: EventChannel? = null - var nowPlayingEventChannel: EventChannel? = null - var playbackVolumeChannel: EventChannel? = null - companion object { - - @Volatile - private var instance: EventChannelSink? = null - - fun getInstance(): EventChannelSink { - if (instance == null) { - synchronized(this) { - if (instance == null) { - instance = EventChannelSink() - } - } - } - return instance!! - } - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/PlaybackService.kt b/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/PlaybackService.kt deleted file mode 100644 index 29d120f..0000000 --- a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/PlaybackService.kt +++ /dev/null @@ -1,154 +0,0 @@ -package me.sithiramunasinghe.flutter.flutter_radio_player.core - -import android.content.Intent -import androidx.annotation.OptIn -import androidx.media3.common.AudioAttributes -import androidx.media3.common.MediaItem -import androidx.media3.common.MediaMetadata -import androidx.media3.common.PlaybackException -import androidx.media3.common.Player -import androidx.media3.common.Player.STATE_IDLE -import androidx.media3.common.Player.STATE_READY -import androidx.media3.common.util.UnstableApi -import androidx.media3.datasource.HttpDataSource.HttpDataSourceException -import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.session.DefaultMediaNotificationProvider -import androidx.media3.session.MediaLibraryService -import androidx.media3.session.MediaSession -import androidx.media3.session.MediaSession.ControllerInfo -import com.google.common.util.concurrent.Futures -import com.google.common.util.concurrent.ListenableFuture -import me.sithiramunasinghe.flutter.flutter_radio_player.FlutterRadioPlayerPlugin.Companion.nowPlayingEventSink -import me.sithiramunasinghe.flutter.flutter_radio_player.FlutterRadioPlayerPlugin.Companion.playBackEventSink -import me.sithiramunasinghe.flutter.flutter_radio_player.FlutterRadioPlayerPlugin.Companion.playbackVolumeControl -import me.sithiramunasinghe.flutter.flutter_radio_player.FlutterRadioPlayerPlugin.Companion.sessionActivity -import me.sithiramunasinghe.flutter.flutter_radio_player.data.FlutterRadioVolumeChanged -import me.sithiramunasinghe.flutter.flutter_radio_player.data.NowPlayingInfo - -class PlaybackService : MediaLibraryService() { - - private lateinit var player: Player - private var mediaSession: MediaLibrarySession? = null - - companion object { - var latestMetadata: MediaMetadata? = null - } - - override fun onCreate() { - super.onCreate() - initializeSessionAndPlayer() - } - - override fun onDestroy() { - if (mediaSession != null) { - mediaSession?.run { - player.release() - mediaSession?.release() - release() - mediaSession = null - } - } - super.onDestroy() - } - - override fun onTaskRemoved(rootIntent: Intent?) { - val player = mediaSession?.player - if (player != null) { -// if (!player.playWhenReady && player.mediaItemCount == 0) { -// stopSelf() -// } - stopSelf() - } - } - - override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession? { - return mediaSession - } - - @OptIn(UnstableApi::class) - private fun initializeSessionAndPlayer() { - - player = ExoPlayer.Builder(this) - .setAudioAttributes(AudioAttributes.DEFAULT, true) - .build() - - mediaSession = - MediaLibrarySession.Builder(this, player, object : MediaLibrarySession.Callback { - override fun onAddMediaItems( - mediaSession: MediaSession, - controller: ControllerInfo, - mediaItems: MutableList - ): ListenableFuture> { - return Futures.immediateFuture(mediaItems) - } - }) - .setSessionActivity(sessionActivity) - .build() - - val default = DefaultMediaNotificationProvider(this) - val appInfo = packageManager.getApplicationInfo(packageName, 0) - default.setSmallIcon(appInfo.icon) - - setMediaNotificationProvider(default) - - player.addListener(object : Player.Listener { - override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { - nowPlayingEventSink?.success(null) - super.onMediaItemTransition(mediaItem, reason) - } - - override fun onIsPlayingChanged(isPlaying: Boolean) { - println("is playing = $isPlaying") - playBackEventSink?.success(isPlaying) - } - - override fun onVolumeChanged(volume: Float) { - println("Volume = $volume") - if (playbackVolumeControl != null) { - playbackVolumeControl!!.success( - FlutterRadioVolumeChanged( - volume = volume, - isMuted = false - ).toJson() - ) - } - super.onVolumeChanged(volume) - } - - override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) { - println("======== TITLE => ${mediaMetadata.title}") - if (nowPlayingEventSink != null) { - var nowPlayingTitle: String? = null - if (mediaMetadata.title != null) { - nowPlayingTitle = mediaMetadata.title.toString() - } - nowPlayingEventSink!!.success( - NowPlayingInfo( - title = nowPlayingTitle, - ).toJson() - ) - latestMetadata = mediaMetadata - } - super.onMediaMetadataChanged(mediaMetadata) - } - - override fun onPlaybackStateChanged(playbackState: Int) { - if (playbackState == STATE_IDLE) { - println("player is idle") - } - - if (playbackState == STATE_READY) { - playBackEventSink?.success(false) - println("player is ready") - } - } - - override fun onPlayerError(error: PlaybackException) { - val cause = error.cause - if (cause is HttpDataSourceException) { - println("player error") - } - } - }) - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/data/FlutterRadioPlayerSource.kt b/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/data/FlutterRadioPlayerSource.kt deleted file mode 100644 index 4558ce5..0000000 --- a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/data/FlutterRadioPlayerSource.kt +++ /dev/null @@ -1,11 +0,0 @@ -package me.sithiramunasinghe.flutter.flutter_radio_player.data - -import kotlinx.serialization.Serializable - -@Serializable -data class FlutterRadioPlayerSource( - var url: String, - var title: String? = null, - var artwork: String? = null, - var playWhenReady: Boolean = false -) diff --git a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/data/FlutterRadioVolumeChanged.kt b/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/data/FlutterRadioVolumeChanged.kt deleted file mode 100644 index 5af020b..0000000 --- a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/data/FlutterRadioVolumeChanged.kt +++ /dev/null @@ -1,12 +0,0 @@ -package me.sithiramunasinghe.flutter.flutter_radio_player.data - -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json - -@Serializable -data class FlutterRadioVolumeChanged(var volume: Float, var isMuted: Boolean) { - fun toJson(): String { - return Json.encodeToString(this) - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/data/NowPlayingInfo.kt b/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/data/NowPlayingInfo.kt deleted file mode 100644 index 975732d..0000000 --- a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/data/NowPlayingInfo.kt +++ /dev/null @@ -1,14 +0,0 @@ -package me.sithiramunasinghe.flutter.flutter_radio_player.data - -import kotlinx.serialization.* -import kotlinx.serialization.json.* - -@Serializable -data class NowPlayingInfo( - val title: String? = null, - val album: String? = null -) { - fun toJson(): String { - return Json.encodeToString(this) - } -} diff --git a/android/src/test/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/FlutterRadioPlayerPluginTest.kt b/android/src/test/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/FlutterRadioPlayerPluginTest.kt deleted file mode 100644 index 7ebefb0..0000000 --- a/android/src/test/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/FlutterRadioPlayerPluginTest.kt +++ /dev/null @@ -1,27 +0,0 @@ -package me.sithiramunasinghe.flutter.flutter_radio_player - -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import kotlin.test.Test -import org.mockito.Mockito - -/* - * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation. - * - * Once you have built the plugin's example app, you can run these tests from the command - * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or - * you can run them directly from IDEs that support JUnit such as Android Studio. - */ - -internal class FlutterRadioPlayerPluginTest { - @Test - fun onMethodCall_getPlatformVersion_returnsExpectedValue() { - val plugin = FlutterRadioPlayerPlugin() - - val call = MethodCall("getPlatformVersion", null) - val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) - plugin.onMethodCall(call, mockResult) - - Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE) - } -} diff --git a/enabling-xcode-bg-service.png b/enabling-xcode-bg-service.png deleted file mode 100644 index cbf0555..0000000 Binary files a/enabling-xcode-bg-service.png and /dev/null differ diff --git a/example/.gitignore b/example/.gitignore index 29a3a50..79c113f 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index ac16c36..1d9ad9e 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -25,12 +25,12 @@ if (flutterVersionName == null) { android { namespace = "me.sithiramunasinghe.flutter.flutter_radio_player_example" - compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion + compileSdk = 35 + ndkVersion = "27.0.12077973" compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } defaultConfig { diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index e1ca574..e496849 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle index adce2b0..607b1a2 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.3.0" apply false - id "org.jetbrains.kotlin.android" version "2.0.0" apply false + id "com.android.application" version "8.9.2" apply false + id "org.jetbrains.kotlin.android" version "2.0.21" apply false } include ":app" diff --git a/example/integration_test/plugin_integration_test.dart b/example/integration_test/plugin_integration_test.dart index 501a542..1a7183b 100644 --- a/example/integration_test/plugin_integration_test.dart +++ b/example/integration_test/plugin_integration_test.dart @@ -1,25 +1,12 @@ -// This is a basic Flutter integration test. -// -// Since integration tests run in a full Flutter application, they can interact -// with the host side of a plugin implementation, unlike Dart unit tests. -// -// For more information about Flutter integration tests, please see -// https://docs.flutter.dev/cookbook/testing/integration/introduction - - import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; - import 'package:flutter_radio_player/flutter_radio_player.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('getPlatformVersion test', (WidgetTester tester) async { - final FlutterRadioPlayer plugin = FlutterRadioPlayer(); - // final String? version = await plugin.getPlatformVersion(); - // The version string depends on the host platform running the test, so - // just assert that some non-empty string is returned. - // expect(version?.isNotEmpty, true); + testWidgets('FlutterRadioPlayer can be instantiated', (WidgetTester tester) async { + final player = FlutterRadioPlayer(); + expect(player, isNotNull); }); } diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 7c56964..1dc6cf7 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/example/ios/Podfile b/example/ios/Podfile index d97f17e..cbb8dfb 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,4 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +platform :ios, '14.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 7fb6bb5..856db99 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,35 +1,28 @@ PODS: - Flutter (1.0.0) - - flutter_radio_player (0.0.1): + - flutter_radio_player_ios (4.0.0): - Flutter - - SwiftAudioEx (~> 1.1.0) - integration_test (0.0.1): - Flutter - - SwiftAudioEx (1.1.0) DEPENDENCIES: - Flutter (from `Flutter`) - - flutter_radio_player (from `.symlinks/plugins/flutter_radio_player/ios`) + - flutter_radio_player_ios (from `.symlinks/plugins/flutter_radio_player_ios/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) -SPEC REPOS: - trunk: - - SwiftAudioEx - EXTERNAL SOURCES: Flutter: :path: Flutter - flutter_radio_player: - :path: ".symlinks/plugins/flutter_radio_player/ios" + flutter_radio_player_ios: + :path: ".symlinks/plugins/flutter_radio_player_ios/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" SPEC CHECKSUMS: - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_radio_player: 2c531f0e2b9e636d6cf09704eb63ad040e3913ff - integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 - SwiftAudioEx: f6aa653770f3a0d3851edaf8d834a30aee4a7646 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + flutter_radio_player_ios: 1adff5797c9c49896c5946d439b757cae7c507e2 + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e -PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 +PODFILE CHECKSUM: ec8b70a489dd1f81e53ef185cf7ad30fce8f8d00 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index d7e238e..43a7eb5 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -7,15 +7,15 @@ objects = { /* Begin PBXBuildFile section */ + 11C4D29F83FFFD3083832305 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 147F3268B170C4C345235ECB /* Pods_Runner.framework */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 676537F3D818096E4D964E7A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1122AB49AD96F5AC9D387982 /* Pods_RunnerTests.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - 9A4679FAAD1D73A1234AC2CF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9209C97E188DE5BE21E99378 /* Pods_Runner.framework */; }; - D8D733DB88B857AA3A9223FF /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 848AF776E3C58B90867FC62E /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -42,20 +42,20 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 04ED878B7DB45851DBA6083A /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - 0610AAD1E76A010FBEE40CDA /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 1122AB49AD96F5AC9D387982 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 147F3268B170C4C345235ECB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 2CFC6F4AF546BC1C79193006 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 2ECDF7AEDFB4A4284267394B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3A74B6B98FE10B3C2C2C0477 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 742EAD42708CB23F05B80A15 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 47773E9112488660D3325277 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 5DAAE1D25C5CE432BF229BCD /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 848AF776E3C58B90867FC62E /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 9209C97E188DE5BE21E99378 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -63,8 +63,8 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A867F295183CBAFC150467E1 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - DA6BF8B3ADC13FEC8889E908 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + E0C2C8EEBCD98AD2776B881F /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + FFE4C89F4F06F3E0301DF255 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -72,7 +72,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9A4679FAAD1D73A1234AC2CF /* Pods_Runner.framework in Frameworks */, + 11C4D29F83FFFD3083832305 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -80,7 +80,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D8D733DB88B857AA3A9223FF /* Pods_RunnerTests.framework in Frameworks */, + 676537F3D818096E4D964E7A /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -95,11 +95,11 @@ path = RunnerTests; sourceTree = ""; }; - 8B0CF0B5276B3AF4FE421B57 /* Frameworks */ = { + 3E9C2853AA8A95A402E3321D /* Frameworks */ = { isa = PBXGroup; children = ( - 9209C97E188DE5BE21E99378 /* Pods_Runner.framework */, - 848AF776E3C58B90867FC62E /* Pods_RunnerTests.framework */, + 147F3268B170C4C345235ECB /* Pods_Runner.framework */, + 1122AB49AD96F5AC9D387982 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -123,7 +123,7 @@ 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, F8C359B2B119EB1906FC1845 /* Pods */, - 8B0CF0B5276B3AF4FE421B57 /* Frameworks */, + 3E9C2853AA8A95A402E3321D /* Frameworks */, ); sourceTree = ""; }; @@ -154,12 +154,12 @@ F8C359B2B119EB1906FC1845 /* Pods */ = { isa = PBXGroup; children = ( - DA6BF8B3ADC13FEC8889E908 /* Pods-Runner.debug.xcconfig */, - 742EAD42708CB23F05B80A15 /* Pods-Runner.release.xcconfig */, - A867F295183CBAFC150467E1 /* Pods-Runner.profile.xcconfig */, - 0610AAD1E76A010FBEE40CDA /* Pods-RunnerTests.debug.xcconfig */, - 2CFC6F4AF546BC1C79193006 /* Pods-RunnerTests.release.xcconfig */, - 04ED878B7DB45851DBA6083A /* Pods-RunnerTests.profile.xcconfig */, + FFE4C89F4F06F3E0301DF255 /* Pods-Runner.debug.xcconfig */, + 47773E9112488660D3325277 /* Pods-Runner.release.xcconfig */, + 3A74B6B98FE10B3C2C2C0477 /* Pods-Runner.profile.xcconfig */, + E0C2C8EEBCD98AD2776B881F /* Pods-RunnerTests.debug.xcconfig */, + 2ECDF7AEDFB4A4284267394B /* Pods-RunnerTests.release.xcconfig */, + 5DAAE1D25C5CE432BF229BCD /* Pods-RunnerTests.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -171,7 +171,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - FE236EBFA829BA06A1FAF7AA /* [CP] Check Pods Manifest.lock */, + 2BF03EEB076FBBF115FA0A71 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, F62A6C3FEF7C0BB1EBBA6A4B /* Frameworks */, @@ -190,14 +190,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 09B73321435E5F50BA5C144F /* [CP] Check Pods Manifest.lock */, + 3EAADE829FDBA18D503A3259 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 2F5A0381B6A2F33BD2D13D5E /* [CP] Embed Pods Frameworks */, + AF6EB656343387972F5BA29B /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -269,7 +269,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 09B73321435E5F50BA5C144F /* [CP] Check Pods Manifest.lock */ = { + 2BF03EEB076FBBF115FA0A71 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -284,45 +284,50 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 2F5A0381B6A2F33BD2D13D5E /* [CP] Embed Pods Frameworks */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + name = "Thin Binary"; + outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 3EAADE829FDBA18D503A3259 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "Thin Binary"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -339,26 +344,21 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - FE236EBFA829BA06A1FAF7AA /* [CP] Check Pods Manifest.lock */ = { + AF6EB656343387972F5BA29B /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -454,7 +454,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -487,7 +487,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 0610AAD1E76A010FBEE40CDA /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = E0C2C8EEBCD98AD2776B881F /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -505,7 +505,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 2CFC6F4AF546BC1C79193006 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 2ECDF7AEDFB4A4284267394B /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -521,7 +521,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 04ED878B7DB45851DBA6083A /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = 5DAAE1D25C5CE432BF229BCD /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -584,7 +584,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -635,7 +635,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8e3ca5d..e3773d4 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 9074fee..6266644 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import Flutter import UIKit -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index da7cf5c..aaa18a8 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -32,6 +32,11 @@ audio + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/example/lib/main.dart b/example/lib/main.dart index a0d288a..24f1099 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,131 +1,256 @@ import 'package:flutter/material.dart'; import 'package:flutter_radio_player/flutter_radio_player.dart'; +const _sources = [ + RadioSource( + url: 'https://s2-webradio.antenne.de/chillout?icy=https', + title: 'Antenne Chillout', + ), + RadioSource( + url: 'https://radio.lotustechnologieslk.net:2020/stream/sunfmgarden?icy=https', + title: 'SunFM - Sri Lanka', + artwork: 'images/sample-cover.jpg', + ), + RadioSource( + url: 'http://stream.riverradio.com:8000/wcvofm.aac', + title: 'River Radio', + ), +]; + void main() { WidgetsFlutterBinding.ensureInitialized(); runApp(const MyApp()); } -class MyApp extends StatefulWidget { +class MyApp extends StatelessWidget { const MyApp({super.key}); @override - State createState() => _MyAppState(); + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Radio Player', + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorSchemeSeed: Colors.deepPurple, + brightness: Brightness.dark, + useMaterial3: true, + ), + home: const PlayerScreen(), + ); + } } -class _MyAppState extends State { - final _flutterRadioPlayerPlugin = FlutterRadioPlayer(); - double volume = 0; +class PlayerScreen extends StatefulWidget { + const PlayerScreen({super.key}); + + @override + State createState() => _PlayerScreenState(); +} + +class _PlayerScreenState extends State { + final _player = FlutterRadioPlayer(); + int _currentSourceIndex = 0; + double _volume = 0.5; @override void initState() { super.initState(); - _flutterRadioPlayerPlugin.initialize( - [ - { - "url": "https://s2-webradio.antenne.de/chillout?icy=https", - }, - { - "title": "SunFM - Sri Lanka", - "artwork": "images/sample-cover.jpg", - "url": - "https://radio.lotustechnologieslk.net:2020/stream/sunfmgarden?icy=https", - }, - {"url": "http://stream.riverradio.com:8000/wcvofm.aac"} - ], - true, - ); + _player.initialize(_sources, playWhenReady: true); + } + + @override + void dispose() { + _player.dispose(); + super.dispose(); + } + + void _onSourceTap(int index) { + setState(() => _currentSourceIndex = index); + _player.jumpToSourceAtIndex(index); } @override Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), - body: Center( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - onPressed: () async { - await _flutterRadioPlayerPlugin.prevSource(); - }, - icon: const Icon(Icons.skip_previous_sharp), - ), - StreamBuilder( - stream: _flutterRadioPlayerPlugin.getPlaybackStream(), - builder: (context, snapshot) { - if (snapshot.hasData) { - return IconButton( - onPressed: () { - if (snapshot.data!) { - _flutterRadioPlayerPlugin.pause(); - } else { - _flutterRadioPlayerPlugin.play(); - } - }, - icon: !snapshot.data! - ? Icon(Icons.play_arrow) - : Icon(Icons.pause), - iconSize: 50.0, - ); - } - return const Text("Player unavailable"); - }, - ), - IconButton( - onPressed: () async { - await _flutterRadioPlayerPlugin.nextSource(); - }, - icon: const Icon(Icons.skip_next_sharp), - ), - ], - ), - StreamBuilder( - stream: _flutterRadioPlayerPlugin.getNowPlayingStream(), - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data?.title != null) { - return Text("Now playing : ${snapshot.data?.title}"); - } - return Text("N/A"); - }, + final colorScheme = Theme.of(context).colorScheme; + + return Scaffold( + body: SafeArea( + child: Column( + children: [ + const SizedBox(height: 32), + // Artwork + Container( + width: 200, + height: 200, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(24), ), - StreamBuilder( - stream: - _flutterRadioPlayerPlugin.getDeviceVolumeChangedStream(), - builder: (context, snapshot) { - if (snapshot.hasData) { - return Text( - "Volume = ${snapshot.data?.volume.floor()} and IsMuted = ${snapshot.data?.isMuted}"); - } - return Text("No Vol data"); - }, + child: Icon( + Icons.radio, + size: 80, + color: colorScheme.primary, ), - FutureBuilder( - future: _flutterRadioPlayerPlugin.getVolume(), - builder: (context, snapshot) { - if (snapshot.hasData) { - return Slider( - value: snapshot.data ?? 0, - min: 0, - max: 1, + ), + const SizedBox(height: 24), + + // Now playing title + StreamBuilder( + stream: _player.nowPlayingStream, + builder: (context, snapshot) { + final title = snapshot.data?.title ?? + _sources[_currentSourceIndex].title ?? + 'Unknown Station'; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleLarge, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + _sources[_currentSourceIndex].title ?? 'Live Radio', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ); + }, + ), + const SizedBox(height: 32), + + // Transport controls + StreamBuilder( + stream: _player.isPlayingStream, + builder: (context, snapshot) { + final isPlaying = snapshot.data ?? false; + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + iconSize: 36, + onPressed: () { + final prev = (_currentSourceIndex - 1 + _sources.length) % + _sources.length; + _onSourceTap(prev); + }, + icon: const Icon(Icons.skip_previous_rounded), + ), + const SizedBox(width: 16), + FilledButton.tonal( + style: FilledButton.styleFrom( + shape: const CircleBorder(), + padding: const EdgeInsets.all(20), + ), + onPressed: () => + isPlaying ? _player.pause() : _player.play(), + child: Icon( + isPlaying + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + size: 40, + ), + ), + const SizedBox(width: 16), + IconButton( + iconSize: 36, + onPressed: () { + final next = + (_currentSourceIndex + 1) % _sources.length; + _onSourceTap(next); + }, + icon: const Icon(Icons.skip_next_rounded), + ), + ], + ); + }, + ), + const SizedBox(height: 24), + + // Volume slider + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Row( + children: [ + Icon(Icons.volume_down_rounded, + color: colorScheme.onSurfaceVariant), + Expanded( + child: Slider( + value: _volume, onChanged: (value) { - setState(() { - volume = value; - _flutterRadioPlayerPlugin.setVolume(volume); - }); + setState(() => _volume = value); + _player.setVolume(value); }, + ), + ), + Icon(Icons.volume_up_rounded, + color: colorScheme.onSurfaceVariant), + ], + ), + ), + + const Spacer(), + + // Source list + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 16, bottom: 8), + child: Text( + 'STATIONS', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: colorScheme.onSurfaceVariant, + letterSpacing: 1.5, + ), + ), + ), + ...List.generate(_sources.length, (index) { + final source = _sources[index]; + final isActive = index == _currentSourceIndex; + return ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + selected: isActive, + selectedTileColor: + colorScheme.primaryContainer.withValues(alpha: 0.3), + leading: CircleAvatar( + backgroundColor: isActive + ? colorScheme.primary + : colorScheme.surfaceContainerHighest, + child: Icon( + isActive + ? Icons.equalizer_rounded + : Icons.radio_rounded, + color: isActive + ? colorScheme.onPrimary + : colorScheme.onSurfaceVariant, + size: 20, + ), + ), + title: Text(source.title ?? source.url), + subtitle: isActive + ? Text('Now playing', + style: TextStyle(color: colorScheme.primary)) + : null, + onTap: () => _onSourceTap(index), ); - } - return Container(); - }, - ) - ], - ), + }), + ], + ), + ), + const SizedBox(height: 16), + ], ), ), ); diff --git a/example/pubspec.lock b/example/pubspec.lock index 8f319d4..a397b19 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -21,26 +21,26 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" cupertino_icons: dependency: "direct main" description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" file: dependency: transitive description: @@ -86,10 +86,31 @@ packages: flutter_radio_player: dependency: "direct main" description: - path: ".." + path: "../packages/flutter_radio_player" relative: true source: path - version: "0.0.1" + version: "4.0.0" + flutter_radio_player_android: + dependency: transitive + description: + path: "../packages/flutter_radio_player_android" + relative: true + source: path + version: "4.0.0" + flutter_radio_player_ios: + dependency: transitive + description: + path: "../packages/flutter_radio_player_ios" + relative: true + source: path + version: "4.0.0" + flutter_radio_player_platform_interface: + dependency: transitive + description: + path: "../packages/flutter_radio_player_platform_interface" + relative: true + source: path + version: "4.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -109,26 +130,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -141,10 +162,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -157,18 +178,18 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.17.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" platform: dependency: transitive description: @@ -197,7 +218,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: @@ -210,18 +231,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: @@ -250,18 +271,18 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.7" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -279,5 +300,5 @@ packages: source: hosted version: "3.0.3" sdks: - dart: ">=3.4.3 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 6770822..e8a81b5 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,32 +1,15 @@ name: flutter_radio_player_example description: "Demonstrates how to use the flutter_radio_player plugin." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: 'none' environment: sdk: '>=3.4.3 <4.0.0' -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter - flutter_radio_player: - # When depending on this package from a real application you should use: - # flutter_radio_player: ^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. - path: ../ - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. + path: ../packages/flutter_radio_player cupertino_icons: ^1.0.6 dev_dependencies: @@ -34,55 +17,9 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. flutter_lints: ^3.0.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - assets: - images/sample-cover.jpg - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/example_player.png b/example_player.png new file mode 100644 index 0000000..01c7f4d Binary files /dev/null and b/example_player.png differ diff --git a/flutter_radio_player_logo.png b/flutter_radio_player_logo.png index d4f5b02..840ee82 100644 Binary files a/flutter_radio_player_logo.png and b/flutter_radio_player_logo.png differ diff --git a/ios/.gitignore b/ios/.gitignore deleted file mode 100644 index 034771f..0000000 --- a/ios/.gitignore +++ /dev/null @@ -1,38 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/Generated.xcconfig -/Flutter/ephemeral/ -/Flutter/flutter_export_environment.sh diff --git a/ios/Assets/.gitkeep b/ios/Assets/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/ios/Classes/FlutterRadioPlayerPlugin.swift b/ios/Classes/FlutterRadioPlayerPlugin.swift deleted file mode 100644 index d4c14c4..0000000 --- a/ios/Classes/FlutterRadioPlayerPlugin.swift +++ /dev/null @@ -1,115 +0,0 @@ -import Flutter -import UIKit - -public class FlutterRadioPlayerPlugin: NSObject, FlutterPlugin { - let player = PlaybackService.instance - static var registrar: FlutterPluginRegistrar? = nil - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "flutter_radio_player", binaryMessenger: registrar.messenger()) - let instance = FlutterRadioPlayerPlugin() - - initalizeChannels(registrar: registrar) - - registrar.addMethodCallDelegate(instance, channel: channel) - FlutterRadioPlayerPlugin.registrar = registrar - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "initialize": - if let args = call.arguments as? Dictionary { - let sourcesAsString = args["sources"] as? String - let isPlayWhenReady = args["playWhenReady"] as? Bool - if let sources = sourcesAsString?.data(using: .utf8) { - let decoder = JSONDecoder() - do { - let sources = try? decoder.decode([FlutterRadioPlayerSource].self, from: sources) - player.intialize(sources: sources!, playWhenReady: isPlayWhenReady!) - } - } - } - break - case "getVolume": - result(player.getVolume()) - break - case "play": - player.play() - break - case "pause": - player.pause() - break - case "nextSource": - player.nextSource() - break - case "prevSource": - player.prevSource() - break - case "changeVolume": - if let args = call.arguments as? Dictionary { - if let volume = args["volume"] as? Double { - player.setVolume(volume: Float(volume)) - break - } - } - case "sourceAtIndex": - if let args = call.arguments as? Dictionary { - if let sourceIndex = args["index"] as? Int { - player.jumpToItem(index: sourceIndex) - break - } - } - default: - result(FlutterMethodNotImplemented) - } - } - - static private func initalizeChannels(registrar: FlutterPluginRegistrar) { - let playbackStatusStream = FlutterEventChannel(name: "flutter_radio_player/playback_status", binaryMessenger: registrar.messenger()) - playbackStatusStream.setStreamHandler(PlaybackStatusEventStreamHandler()) - - let nowPlayingInfoStream = FlutterEventChannel(name: "flutter_radio_player/now_playing_info", binaryMessenger: registrar.messenger()) - nowPlayingInfoStream.setStreamHandler(NowPlayingInfoStreamHandler()) - - let deviceVolumeControlStream = FlutterEventChannel(name: "flutter_radio_player/volume_control", binaryMessenger: registrar.messenger()) - deviceVolumeControlStream.setStreamHandler(DeviceVolumeStreamHandler()) - } -} - -class PlaybackStatusEventStreamHandler: NSObject, FlutterStreamHandler { - func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - PlaybackService.instance.playBackEventSink = events - return nil - } - - func onCancel(withArguments arguments: Any?) -> FlutterError? { - PlaybackService.instance.playBackEventSink = nil - return nil - } -} - - -class NowPlayingInfoStreamHandler: NSObject, FlutterStreamHandler { - func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - PlaybackService.instance.nowPlayingEventSink = events - return nil - } - - func onCancel(withArguments arguments: Any?) -> FlutterError? { - PlaybackService.instance.nowPlayingEventSink = nil - return nil - } -} - -class DeviceVolumeStreamHandler: NSObject, FlutterStreamHandler { - func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - PlaybackService.instance.playbackVolumeControl = events - return nil - } - - func onCancel(withArguments arguments: Any?) -> FlutterError? { - PlaybackService.instance.playbackVolumeControl = nil - return nil - } -} - diff --git a/ios/Classes/core/EventChannelSink.swift b/ios/Classes/core/EventChannelSink.swift deleted file mode 100644 index d46a61f..0000000 --- a/ios/Classes/core/EventChannelSink.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// EventChannelSink.swift -// flutter_radio_player -// -// Created by Sithira Munasinghe on 2024-07-23. -// - -import Foundation -import Flutter - -class EventChannelSink { - static let instance = EventChannelSink() - var playbackEventChannel: FlutterEventChannel?; - var nowPlayingEventChannel: FlutterEventChannel?; - var playbackVolumeChannel: FlutterEventChannel?; - - private init() { - - } - - -} diff --git a/ios/Classes/core/PlaybackService.swift b/ios/Classes/core/PlaybackService.swift deleted file mode 100644 index 866d919..0000000 --- a/ios/Classes/core/PlaybackService.swift +++ /dev/null @@ -1,158 +0,0 @@ -// -// PlaybackService.swift -// flutter_radio_player -// -// Created by Sithira Munasinghe on 2024-07-23. -// - -import Foundation -import SwiftAudioEx -import AVFoundation -import MediaPlayer -import Flutter - -class PlaybackService: NSObject { - static let instance = PlaybackService() - private var player: QueuedAudioPlayer? - var playBackEventSink: FlutterEventSink? = nil - var nowPlayingEventSink: FlutterEventSink? = nil - var playbackVolumeControl: FlutterEventSink? = nil - let audioSession = AVAudioSession.sharedInstance() - - private override init() { - super.init() - player = QueuedAudioPlayer() - player?.automaticallyUpdateNowPlayingInfo = true - player?.remoteCommands = [ - .play, - .pause, - .next, - .previous - ] - - player?.nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.isLiveStream(true)) - player?.volume = 0.5 - - do { - try AudioSessionController.shared.set(category: .playback) - try AudioSessionController.shared.activateSession() - } catch { - - } - - player?.event.stateChange.addListener(self, handlePlayerStateChange) - player?.event.receiveTimedMetadata.addListener(self, handleNowPlayingChanges) - player?.event.receiveCommonMetadata.addListener(self, handleCommonChanges) - - UIApplication.shared.beginReceivingRemoteControlEvents() - } - - - private func handleCommonChanges(data: Any) { - print(data) - } - - func intialize(sources: Array, playWhenReady: Bool) { - for source in sources { - let mediaSource = DefaultAudioItem(audioUrl: source.url, sourceType: .stream) - if source.title == nil { - mediaSource.artist = getAppName() - } else { - mediaSource.title = source.title - mediaSource.artist = getAppName() - } - if source.artwork != nil { - mediaSource.artwork = loadImageFromFlutterAssets(assetName: source.artwork!, registrar: FlutterRadioPlayerPlugin.registrar!) - } - player?.add(item: mediaSource, playWhenReady: playWhenReady) - } - } - - func play() { - player?.play() - } - - func pause() { - if player?.playerState.rawValue == "playing" { - player?.pause() - } - } - - func nextSource() { - if player?.items != nil { - player?.next() - player?.nowPlayingInfoController.set(keyValue: MediaItemProperty.artist(player?.currentItem?.getArtist())) - self.nowPlayingEventSink?(nil) - } - } - - func prevSource() { - if player?.items != nil { - player?.previous() - self.nowPlayingEventSink?(nil) - } - } - - func getVolume() -> Float { - return (player?.volume)! - } - - func setVolume(volume: Float) { - player?.volume = volume - DispatchQueue.main.async { - let preparedEvent = FlutterRadioVolumeChanged(volume: volume) - if let data = try? JSONEncoder().encode(preparedEvent) { - self.playbackVolumeControl?(String(data: data, encoding: .utf8)) - } - } - self.playbackVolumeControl?(volume) - } - - func jumpToItem(index: Int) { - try? player?.jumpToItem(atIndex: index, playWhenReady: player?.playWhenReady) - } - - private func handleNowPlayingChanges(data: Array) { - let nowPlayingData = data.first?.items.first - if let nowPlayingAVMetaTitle = nowPlayingData?.value { - player?.nowPlayingInfoController.set(keyValue: MediaItemProperty.title(nowPlayingAVMetaTitle as? String)) - let nowPlaying = NowPlayingInfo(title: nowPlayingAVMetaTitle as? String) - if let encodedData = try? JSONEncoder().encode(nowPlaying) { - DispatchQueue.main.async { - self.nowPlayingEventSink?(String(data: encodedData, encoding: .utf8)!) - } - } - } - } - - private func handlePlayerStateChange(state: AVPlayerWrapperState) { - if state.rawValue == "playing" { - DispatchQueue.main.async { - self.playBackEventSink?(true) - } - } - if state.rawValue == "paused" { - DispatchQueue.main.async { - self.playBackEventSink?(false) - } - } - } - - private func getAppName() -> String? { - if let appName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? Bundle.main.infoDictionary?["CFBundleName"] as? String { - return appName - } else { - return nil - } - } - - private func loadImageFromFlutterAssets(assetName: String, registrar: FlutterPluginRegistrar) -> UIImage? { - let assetKey = registrar.lookupKey(forAsset: assetName) - guard let assetPath = Bundle.main.path(forResource: assetKey, ofType: nil), - let image = UIImage(contentsOfFile: assetPath) else { - return nil - } - return image - } - -} diff --git a/ios/Classes/data/FlutterRadioPlayerSource.swift b/ios/Classes/data/FlutterRadioPlayerSource.swift deleted file mode 100644 index 3190898..0000000 --- a/ios/Classes/data/FlutterRadioPlayerSource.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// FlutterRadioPlayerSource.swift -// flutter_radio_player -// -// Created by Sithira Munasinghe on 2024-07-24. -// - -import Foundation - -struct FlutterRadioPlayerSource: Decodable { - var url: String - var title: String? - var artwork: String? -} diff --git a/ios/Classes/data/FlutterRadioVolumeChange.swift b/ios/Classes/data/FlutterRadioVolumeChange.swift deleted file mode 100644 index 371a708..0000000 --- a/ios/Classes/data/FlutterRadioVolumeChange.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// FlutterRadioVolumeChange.swift -// flutter_radio_player -// -// Created by Sithira Munasinghe on 2024-07-24. -// - -import Foundation - -struct FlutterRadioVolumeChanged: Codable { - var volume: Float - var isMuted: Bool = false -} diff --git a/ios/Classes/data/NowPlayingInfo.swift b/ios/Classes/data/NowPlayingInfo.swift deleted file mode 100644 index 7f62fab..0000000 --- a/ios/Classes/data/NowPlayingInfo.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// NowPlayingInfo.swift -// flutter_radio_player -// -// Created by Sithira Munasinghe on 2024-07-24. -// - -import Foundation - -struct NowPlayingInfo: Codable { - var title: String? = nil -} diff --git a/ios/flutter_radio_player.podspec b/ios/flutter_radio_player.podspec deleted file mode 100644 index e589f48..0000000 --- a/ios/flutter_radio_player.podspec +++ /dev/null @@ -1,25 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint flutter_radio_player.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'flutter_radio_player' - s.version = '0.0.1' - s.summary = 'Online Radio Player for Flutter which enable to play streaming URL. Supports Android and iOS as well as WearOs and watchOs' - s.description = <<-DESC -Online Radio Player for Flutter which enable to play streaming URL. Supports Android and iOS as well as WearOs and watchOs - DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'Flutter' - s.dependency 'SwiftAudioEx', '~> 1.1.0' - s.platform = :ios, '12.0' - - # Flutter.framework does not contain a i386 slice. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } - s.swift_version = '5.0' -end - diff --git a/lib/data/flutter_radio_player_event.dart b/lib/data/flutter_radio_player_event.dart deleted file mode 100644 index 0e1494b..0000000 --- a/lib/data/flutter_radio_player_event.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'dart:convert'; - -class NowPlayingDataChanged { - final String? title; - - NowPlayingDataChanged({ - required this.title, - }); - - factory NowPlayingDataChanged.fromJson(String data) { - var json = jsonDecode(data); - return NowPlayingDataChanged( - title: json['title'], - ); - } -} - -class DeviceVolumeDataChanged { - final double volume; - final bool? isMuted; - - DeviceVolumeDataChanged({required this.volume, required this.isMuted}); - - factory DeviceVolumeDataChanged.fromEvent(String data) { - var json = jsonDecode(data); - return DeviceVolumeDataChanged( - volume: json['volume'], isMuted: json['isMuted']); - } -} diff --git a/lib/flutter_radio_player.dart b/lib/flutter_radio_player.dart deleted file mode 100644 index 34b285e..0000000 --- a/lib/flutter_radio_player.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'data/flutter_radio_player_event.dart'; -import 'flutter_radio_player_platform_interface.dart'; - -class FlutterRadioPlayer { - Future initialize( - List> sources, bool playWhenReady) async { - FlutterRadioPlayerPlatform.instance.initialize(sources, playWhenReady); - } - - Future play() { - return FlutterRadioPlayerPlatform.instance.play(); - } - - Future pause() { - return FlutterRadioPlayerPlatform.instance.pause(); - } - - Future setVolume(double volume) { - return FlutterRadioPlayerPlatform.instance.changeVolume(volume); - } - - Future getVolume() { - return FlutterRadioPlayerPlatform.instance.getVolume(); - } - - Future nextSource() { - return FlutterRadioPlayerPlatform.instance.nextSource(); - } - - Future prevSource() { - return FlutterRadioPlayerPlatform.instance.previousSource(); - } - - Future jumpToSourceIndex(int index) { - return FlutterRadioPlayerPlatform.instance.jumpToSourceIndex(index); - } - - Stream getPlaybackStream() => - FlutterRadioPlayerPlatform.instance.getIsPlayingStream(); - - Stream getNowPlayingStream() => - FlutterRadioPlayerPlatform.instance.getNowPlayingStream(); - - Stream getDeviceVolumeChangedStream() => - FlutterRadioPlayerPlatform.instance.getDeviceVolumeChangedStream(); -} diff --git a/lib/flutter_radio_player_method_channel.dart b/lib/flutter_radio_player_method_channel.dart deleted file mode 100644 index 6f3f85b..0000000 --- a/lib/flutter_radio_player_method_channel.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_radio_player/data/flutter_radio_player_event.dart'; - -import 'flutter_radio_player_platform_interface.dart'; - -/// An implementation of [FlutterRadioPlayerPlatform] that uses method channels. -class MethodChannelFlutterRadioPlayer extends FlutterRadioPlayerPlatform { - @visibleForTesting - final methodChannel = const MethodChannel('flutter_radio_player'); - - static const playbackStatusEventChannel = - EventChannel("flutter_radio_player/playback_status"); - - static const nowPlayingInfoEventChannel = - EventChannel("flutter_radio_player/now_playing_info"); - - static const deviceVolumeChangedEventChannel = - EventChannel("flutter_radio_player/volume_control"); - - Stream? _playbackStream; - - Stream? _nowPlayingInfo; - - Stream? _deviceVolumeChangedStream; - - @override - Future initialize( - List> sources, bool playWhenReady) async { - await methodChannel.invokeMethod('initialize', { - "sources": jsonEncode(sources), - "playWhenReady": playWhenReady, - }); - } - - @override - Future play() async { - await methodChannel.invokeMethod("play"); - } - - @override - Future pause() async { - await methodChannel.invokeMethod("pause"); - } - - @override - Future playOrPause() async { - await methodChannel.invokeMethod("playOrPause"); - } - - @override - Future changeVolume(double volume) async { - await methodChannel.invokeMethod("changeVolume", {"volume": volume}); - } - - @override - Future getVolume() async { - return await methodChannel.invokeMethod("getVolume"); - } - - @override - Future nextSource() async { - await methodChannel.invokeMethod("nextSource"); - } - - @override - Future previousSource() async { - await methodChannel.invokeMethod("prevSource"); - } - - @override - Future jumpToSourceIndex(int index) async { - await methodChannel.invokeMethod("sourceAtIndex", {"index": index}); - } - - @override - Stream getIsPlayingStream() { - if (_playbackStream != null) { - return _playbackStream!; - } - - var playbackStream = playbackStatusEventChannel - .receiveBroadcastStream() - .asBroadcastStream(onCancel: (sub) { - sub.cancel(); - _playbackStream = null; - }); - - return playbackStream.map( - (dynamic element) { - return element as bool; - }, - ); - } - - @override - Stream getNowPlayingStream() { - if (_nowPlayingInfo != null) { - return _nowPlayingInfo!; - } - - var playerReadyStream = nowPlayingInfoEventChannel - .receiveBroadcastStream() - .asBroadcastStream(onCancel: (sub) { - sub.cancel(); - _nowPlayingInfo = null; - }); - - return playerReadyStream.map((dynamic event) { - return NowPlayingDataChanged.fromJson(event as String); - }); - } - - @override - Stream getDeviceVolumeChangedStream() { - if (_deviceVolumeChangedStream != null) { - return _deviceVolumeChangedStream!; - } - var deviceVolumeChangedStream = deviceVolumeChangedEventChannel - .receiveBroadcastStream() - .asBroadcastStream(onCancel: (sub) { - sub.cancel(); - _deviceVolumeChangedStream = null; - }); - - return deviceVolumeChangedStream.map((dynamic event) { - return DeviceVolumeDataChanged.fromEvent(event as String); - }); - } -} diff --git a/lib/flutter_radio_player_platform_interface.dart b/lib/flutter_radio_player_platform_interface.dart deleted file mode 100644 index e7481a4..0000000 --- a/lib/flutter_radio_player_platform_interface.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -import 'data/flutter_radio_player_event.dart'; -import 'flutter_radio_player_method_channel.dart'; - -abstract class FlutterRadioPlayerPlatform extends PlatformInterface { - /// Constructs a FlutterRadioPlayerPlatform. - FlutterRadioPlayerPlatform() : super(token: _token); - - static final Object _token = Object(); - - static FlutterRadioPlayerPlatform _instance = - MethodChannelFlutterRadioPlayer(); - - /// The default instance of [FlutterRadioPlayerPlatform] to use. - /// - /// Defaults to [MethodChannelFlutterRadioPlayer]. - static FlutterRadioPlayerPlatform get instance => _instance; - - /// Platform-specific implementations should set this with their own - /// platform-specific class that extends [FlutterRadioPlayerPlatform] when - /// they register themselves. - static set instance(FlutterRadioPlayerPlatform instance) { - PlatformInterface.verifyToken(instance, _token); - _instance = instance; - } - - /// Initialize flutter radio player - Future initialize( - List> sources, bool playWhenReady) { - throw UnimplementedError('initialize() has not been implemented.'); - } - - /// Play the media source - Future play() { - throw UnimplementedError('play() has not been implemented.'); - } - - /// Pause the media source - Future pause() { - throw UnimplementedError('pause() has not been implemented.'); - } - - /// Either play or pause depending on the play state - Future playOrPause() { - throw UnimplementedError('playOrPause() has not been implemented.'); - } - - /// Change the player volume - Future changeVolume(double volume); - - /// Change the next source in the sources index - Future nextSource(); - - /// Change the previous source in the sources index - Future previousSource(); - - /// Jump to source at a index - Future jumpToSourceIndex(int index); - - /// Get the current volume of the player. Defaults to 0.5 (low: 0, max: 1) - Future getVolume(); - - /// Playback stream - Stream getIsPlayingStream(); - - /// Now playing stream of icy / meta info - Stream getNowPlayingStream(); - - /// Stream of player volume changes - Stream getDeviceVolumeChangedStream(); -} diff --git a/packages/flutter_radio_player/analysis_options.yaml b/packages/flutter_radio_player/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/packages/flutter_radio_player/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/packages/flutter_radio_player/lib/flutter_radio_player.dart b/packages/flutter_radio_player/lib/flutter_radio_player.dart new file mode 100644 index 0000000..38d510c --- /dev/null +++ b/packages/flutter_radio_player/lib/flutter_radio_player.dart @@ -0,0 +1,41 @@ +export 'package:flutter_radio_player_platform_interface/flutter_radio_player_platform_interface.dart' + show RadioSource, NowPlayingInfo, VolumeInfo; + +import 'package:flutter_radio_player_platform_interface/flutter_radio_player_platform_interface.dart'; + +class FlutterRadioPlayer { + FlutterRadioPlayerPlatform get _platform => + FlutterRadioPlayerPlatform.instance; + + Future initialize( + List sources, { + bool playWhenReady = false, + }) { + return _platform.initialize(sources, playWhenReady: playWhenReady); + } + + Future play() => _platform.play(); + + Future pause() => _platform.pause(); + + Future playOrPause() => _platform.playOrPause(); + + Future setVolume(double volume) => _platform.setVolume(volume); + + Future getVolume() => _platform.getVolume(); + + Future nextSource() => _platform.nextSource(); + + Future previousSource() => _platform.previousSource(); + + Future jumpToSourceAtIndex(int index) => + _platform.jumpToSourceAtIndex(index); + + Future dispose() => _platform.dispose(); + + Stream get isPlayingStream => _platform.isPlayingStream; + + Stream get nowPlayingStream => _platform.nowPlayingStream; + + Stream get volumeStream => _platform.volumeStream; +} diff --git a/packages/flutter_radio_player/pubspec.lock b/packages/flutter_radio_player/pubspec.lock new file mode 100644 index 0000000..f4a8fcc --- /dev/null +++ b/packages/flutter_radio_player/pubspec.lock @@ -0,0 +1,234 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + flutter_radio_player_android: + dependency: "direct main" + description: + path: "../flutter_radio_player_android" + relative: true + source: path + version: "4.0.0" + flutter_radio_player_ios: + dependency: "direct main" + description: + path: "../flutter_radio_player_ios" + relative: true + source: path + version: "4.0.0" + flutter_radio_player_platform_interface: + dependency: "direct main" + description: + path: "../flutter_radio_player_platform_interface" + relative: true + source: path + version: "4.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + plugin_platform_interface: + dependency: "direct dev" + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" +sdks: + dart: ">=3.8.0-0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/packages/flutter_radio_player/pubspec.yaml b/packages/flutter_radio_player/pubspec.yaml new file mode 100644 index 0000000..2714ac5 --- /dev/null +++ b/packages/flutter_radio_player/pubspec.yaml @@ -0,0 +1,32 @@ +name: flutter_radio_player +description: "Online Radio Player for Flutter with background playback, lock screen controls, and streaming support for Android and iOS." +version: 4.0.0 +homepage: https://github.com/Sithira/FlutterRadioPlayer + +environment: + sdk: '>=3.4.3 <4.0.0' + flutter: '>=3.3.0' + +flutter: + plugin: + platforms: + android: + default_package: flutter_radio_player_android + ios: + default_package: flutter_radio_player_ios + +dependencies: + flutter: + sdk: flutter + flutter_radio_player_platform_interface: + path: ../flutter_radio_player_platform_interface + flutter_radio_player_android: + path: ../flutter_radio_player_android + flutter_radio_player_ios: + path: ../flutter_radio_player_ios + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 + plugin_platform_interface: ^2.1.7 diff --git a/packages/flutter_radio_player/test/flutter_radio_player_test.dart b/packages/flutter_radio_player/test/flutter_radio_player_test.dart new file mode 100644 index 0000000..30d0ce0 --- /dev/null +++ b/packages/flutter_radio_player/test/flutter_radio_player_test.dart @@ -0,0 +1,123 @@ +import 'dart:async'; + +import 'package:flutter_radio_player/flutter_radio_player.dart'; +import 'package:flutter_radio_player_platform_interface/flutter_radio_player_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class FakePlatform extends FlutterRadioPlayerPlatform + with MockPlatformInterfaceMixin { + List calls = []; + + @override + Future initialize(List sources, + {bool playWhenReady = false}) async { + calls.add('initialize'); + } + + @override + Future play() async => calls.add('play'); + @override + Future pause() async => calls.add('pause'); + @override + Future playOrPause() async => calls.add('playOrPause'); + @override + Future setVolume(double volume) async => calls.add('setVolume'); + @override + Future getVolume() async => 0.5; + @override + Future nextSource() async => calls.add('nextSource'); + @override + Future previousSource() async => calls.add('previousSource'); + @override + Future jumpToSourceAtIndex(int index) async => + calls.add('jumpToSourceAtIndex'); + @override + Future dispose() async => calls.add('dispose'); + + @override + Stream get isPlayingStream => Stream.value(true); + @override + Stream get nowPlayingStream => + Stream.value(const NowPlayingInfo(title: 'Test')); + @override + Stream get volumeStream => + Stream.value(const VolumeInfo(volume: 0.5, isMuted: false)); +} + +void main() { + late FakePlatform fakePlatform; + late FlutterRadioPlayer player; + + setUp(() { + fakePlatform = FakePlatform(); + FlutterRadioPlayerPlatform.instance = fakePlatform; + player = FlutterRadioPlayer(); + }); + + test('initialize delegates to platform', () async { + await player.initialize([const RadioSource(url: 'http://test.com')]); + expect(fakePlatform.calls, contains('initialize')); + }); + + test('play delegates to platform', () async { + await player.play(); + expect(fakePlatform.calls, contains('play')); + }); + + test('pause delegates to platform', () async { + await player.pause(); + expect(fakePlatform.calls, contains('pause')); + }); + + test('playOrPause delegates to platform', () async { + await player.playOrPause(); + expect(fakePlatform.calls, contains('playOrPause')); + }); + + test('setVolume delegates to platform', () async { + await player.setVolume(0.8); + expect(fakePlatform.calls, contains('setVolume')); + }); + + test('getVolume returns value from platform', () async { + final volume = await player.getVolume(); + expect(volume, 0.5); + }); + + test('nextSource delegates to platform', () async { + await player.nextSource(); + expect(fakePlatform.calls, contains('nextSource')); + }); + + test('previousSource delegates to platform', () async { + await player.previousSource(); + expect(fakePlatform.calls, contains('previousSource')); + }); + + test('jumpToSourceAtIndex delegates to platform', () async { + await player.jumpToSourceAtIndex(1); + expect(fakePlatform.calls, contains('jumpToSourceAtIndex')); + }); + + test('dispose delegates to platform', () async { + await player.dispose(); + expect(fakePlatform.calls, contains('dispose')); + }); + + test('isPlayingStream returns stream from platform', () async { + final value = await player.isPlayingStream.first; + expect(value, true); + }); + + test('nowPlayingStream returns stream from platform', () async { + final value = await player.nowPlayingStream.first; + expect(value.title, 'Test'); + }); + + test('volumeStream returns stream from platform', () async { + final value = await player.volumeStream.first; + expect(value.volume, 0.5); + expect(value.isMuted, false); + }); +} diff --git a/packages/flutter_radio_player_android/analysis_options.yaml b/packages/flutter_radio_player_android/analysis_options.yaml new file mode 100644 index 0000000..ed7644f --- /dev/null +++ b/packages/flutter_radio_player_android/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + - "lib/src/messages.g.dart" diff --git a/android/build.gradle b/packages/flutter_radio_player_android/android/build.gradle similarity index 61% rename from android/build.gradle rename to packages/flutter_radio_player_android/android/build.gradle index 720f4f2..2dbd577 100644 --- a/android/build.gradle +++ b/packages/flutter_radio_player_android/android/build.gradle @@ -2,15 +2,13 @@ group = "me.sithiramunasinghe.flutter.flutter_radio_player" version = "1.0-SNAPSHOT" buildscript { - ext.kotlin_version = "2.0.0" + ext.kotlin_version = "2.1.20" repositories { google() mavenCentral() } - dependencies { - classpath("com.android.tools.build:gradle:7.3.0") - classpath("org.jetbrains.kotlin:kotlin-serialization:$kotlin_version") + classpath("com.android.tools.build:gradle:8.9.2") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") } } @@ -24,22 +22,21 @@ allprojects { apply plugin: "com.android.library" apply plugin: "kotlin-android" -apply plugin: 'kotlinx-serialization' android { if (project.android.hasProperty("namespace")) { namespace = "me.sithiramunasinghe.flutter.flutter_radio_player" } - compileSdk = 34 + compileSdk = 35 compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "21" } sourceSets { @@ -52,20 +49,18 @@ android { } dependencies { - implementation("androidx.media3:media3-exoplayer:1.3.1") - implementation("androidx.media3:media3-exoplayer-hls:1.3.1") - implementation("androidx.media3:media3-session:1.3.1") - implementation "androidx.media3:media3-common:1.3.1" - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") + implementation("androidx.media3:media3-exoplayer:1.6.1") + implementation("androidx.media3:media3-exoplayer-hls:1.6.1") + implementation("androidx.media3:media3-session:1.6.1") + implementation("androidx.media3:media3-common:1.6.1") testImplementation("org.jetbrains.kotlin:kotlin-test") - testImplementation("org.mockito:mockito-core:5.0.0") + testImplementation("org.mockito:mockito-core:5.14.2") } testOptions { unitTests.all { useJUnitPlatform() - testLogging { events "passed", "skipped", "failed", "standardOut", "standardError" outputs.upToDateWhen { false } diff --git a/packages/flutter_radio_player_android/android/settings.gradle b/packages/flutter_radio_player_android/android/settings.gradle new file mode 100644 index 0000000..c34e4f6 --- /dev/null +++ b/packages/flutter_radio_player_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'flutter_radio_player_android' diff --git a/android/src/main/AndroidManifest.xml b/packages/flutter_radio_player_android/android/src/main/AndroidManifest.xml similarity index 90% rename from android/src/main/AndroidManifest.xml rename to packages/flutter_radio_player_android/android/src/main/AndroidManifest.xml index 2b0e9dc..d70427f 100644 --- a/android/src/main/AndroidManifest.xml +++ b/packages/flutter_radio_player_android/android/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + Unit>() + + private var playbackStateSink: PigeonEventSink? = null + private var nowPlayingSink: PigeonEventSink? = null + private var volumeSink: PigeonEventSink? = null + + companion object { + lateinit var sessionActivity: PendingIntent + + private fun getSessionActivity(context: Context, activity: Activity) { + sessionActivity = PendingIntent.getActivity( + context, 0, Intent(context, activity::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } + } + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + applicationContext = flutterPluginBinding.applicationContext + RadioPlayerHostApi.setUp(flutterPluginBinding.binaryMessenger, this) + setupEventChannels(flutterPluginBinding) + + val token = SessionToken( + flutterPluginBinding.applicationContext, + ComponentName(flutterPluginBinding.applicationContext, PlaybackService::class.java) + ) + + val mediaControllerFuture = MediaController.Builder(applicationContext!!, token).buildAsync() + mediaControllerFuture.addListener({ + mediaController = mediaControllerFuture.get() + isMediaControllerAvailable = true + PlaybackService.playbackStateSink = playbackStateSink + PlaybackService.nowPlayingSink = nowPlayingSink + PlaybackService.volumeSink = volumeSink + executePendingOperations() + }, MoreExecutors.directExecutor()) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + RadioPlayerHostApi.setUp(binding.binaryMessenger, null) + PlaybackService.playbackStateSink = null + PlaybackService.nowPlayingSink = null + PlaybackService.volumeSink = null + playbackStateSink = null + nowPlayingSink = null + volumeSink = null + mediaController?.release() + mediaController = null + isMediaControllerAvailable = false + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + getSessionActivity(binding.activity.applicationContext, binding.activity) + } + + override fun onDetachedFromActivityForConfigChanges() {} + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + getSessionActivity(binding.activity.applicationContext, binding.activity) + } + override fun onDetachedFromActivity() {} + + @OptIn(UnstableApi::class) + override fun initialize(sources: List, playWhenReady: Boolean) { + withMediaController { controller -> + if (controller.isPlaying) { + playbackStateSink?.success(true) + val title = PlaybackService.latestMetadata?.title?.toString() + nowPlayingSink?.success(NowPlayingInfoMessage(title = title)) + return@withMediaController + } + + controller.volume = 0.5F + controller.playWhenReady = playWhenReady + + if (sources.isNotEmpty()) { + controller.setMediaItems(sources.map { buildMediaItem(it) }) + controller.prepare() + } + } + } + + override fun play() { + withMediaController { it.play() } + } + + override fun pause() { + withMediaController { it.pause() } + } + + override fun playOrPause() { + withMediaController { controller -> + if (controller.mediaItemCount != 0) { + if (controller.isPlaying) controller.pause() else controller.play() + } + } + } + + override fun setVolume(volume: Double) { + withMediaController { it.volume = volume.toFloat() } + } + + override fun getVolume(): Double { + return mediaController?.volume?.toDouble() ?: 0.5 + } + + override fun nextSource() { + withMediaController { + PlaybackService.latestMetadata = null + it.seekToNextMediaItem() + } + } + + override fun previousSource() { + withMediaController { + PlaybackService.latestMetadata = null + it.seekToPreviousMediaItem() + } + } + + override fun jumpToSourceAtIndex(index: Long) { + withMediaController { + PlaybackService.latestMetadata = null + it.seekToDefaultPosition(index.toInt()) + } + } + + override fun dispose() { + mediaController?.run { + stop() + release() + } + mediaController = null + isMediaControllerAvailable = false + } + + private fun setupEventChannels(binding: FlutterPlugin.FlutterPluginBinding) { + OnPlaybackStateChangedStreamHandler.register( + binding.binaryMessenger, + object : OnPlaybackStateChangedStreamHandler() { + override fun onListen(arguments: Any?, sink: PigeonEventSink) { + playbackStateSink = sink + PlaybackService.playbackStateSink = sink + executePendingOperations() + } + override fun onCancel(arguments: Any?) { + playbackStateSink = null + PlaybackService.playbackStateSink = null + } + } + ) + + OnNowPlayingChangedStreamHandler.register( + binding.binaryMessenger, + object : OnNowPlayingChangedStreamHandler() { + override fun onListen(arguments: Any?, sink: PigeonEventSink) { + nowPlayingSink = sink + PlaybackService.nowPlayingSink = sink + executePendingOperations() + } + override fun onCancel(arguments: Any?) { + nowPlayingSink = null + PlaybackService.nowPlayingSink = null + } + } + ) + + OnVolumeChangedStreamHandler.register( + binding.binaryMessenger, + object : OnVolumeChangedStreamHandler() { + override fun onListen(arguments: Any?, sink: PigeonEventSink) { + volumeSink = sink + PlaybackService.volumeSink = sink + } + override fun onCancel(arguments: Any?) { + volumeSink = null + PlaybackService.volumeSink = null + } + } + ) + } + + private fun withMediaController(action: (MediaController) -> Unit) { + if (isMediaControllerAvailable && mediaController != null) { + action(mediaController!!) + } else { + pendingOperations.add { action(mediaController!!) } + } + } + + private fun executePendingOperations() { + if (!isMediaControllerAvailable || mediaController == null) return + pendingOperations.forEach { it() } + pendingOperations.clear() + } + + @OptIn(UnstableApi::class) + private fun buildMediaItem(source: RadioSourceMessage): MediaItem { + val metaBuilder = MediaMetadata.Builder() + if (source.title.isNullOrEmpty()) { + metaBuilder.setArtist(getAppName()) + } else { + metaBuilder.setTitle(source.title) + metaBuilder.setArtist(getAppName()) + } + if (!source.artwork.isNullOrEmpty()) { + if (source.artwork!!.contains("http")) { + metaBuilder.setArtworkUri(source.artwork!!.toUri()) + } else { + val stream = loadFlutterAsset(source.artwork) + if (stream != null) { + metaBuilder.setArtworkData( + ByteStreams.toByteArray(stream), + MediaMetadata.PICTURE_TYPE_FRONT_COVER + ) + } + } + } + return MediaItem.Builder() + .setUri(source.url) + .setMediaMetadata(metaBuilder.build()) + .build() + } + + private fun loadFlutterAsset(assetPath: String?): InputStream? { + return try { + val flutterLoader = FlutterLoader() + flutterLoader.startInitialization(applicationContext!!) + flutterLoader.ensureInitializationComplete(applicationContext!!, null) + val key = flutterLoader.getLookupKeyForAsset(assetPath!!) + val assetManager: AssetManager = applicationContext!!.assets + assetManager.open(key) + } catch (e: Exception) { + null + } + } + + private fun getAppName(): String? { + return try { + val pm: PackageManager = applicationContext!!.packageManager + val ai = pm.getApplicationInfo(applicationContext!!.packageName, 0) + pm.getApplicationLabel(ai) as String + } catch (e: PackageManager.NameNotFoundException) { + null + } + } +} diff --git a/packages/flutter_radio_player_android/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/Messages.g.kt b/packages/flutter_radio_player_android/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/Messages.g.kt new file mode 100644 index 0000000..527fb46 --- /dev/null +++ b/packages/flutter_radio_player_android/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/Messages.g.kt @@ -0,0 +1,493 @@ +// Autogenerated from Pigeon (v26.1.7), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package me.sithiramunasinghe.flutter.flutter_radio_player + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMethodCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +private object MessagesPigeonUtils { + + fun wrapResult(result: Any?): List { + return listOf(result) + } + + fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } + } + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a is ByteArray && b is ByteArray) { + return a.contentEquals(b) + } + if (a is IntArray && b is IntArray) { + return a.contentEquals(b) + } + if (a is LongArray && b is LongArray) { + return a.contentEquals(b) + } + if (a is DoubleArray && b is DoubleArray) { + return a.contentEquals(b) + } + if (a is Array<*> && b is Array<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is List<*> && b is List<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is Map<*, *> && b is Map<*, *>) { + return a.size == b.size && a.all { + (b as Map).contains(it.key) && + deepEquals(it.value, b[it.key]) + } + } + return a == b + } + +} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError ( + val code: String, + override val message: String? = null, + val details: Any? = null +) : Throwable() + +/** Generated class from Pigeon that represents data sent in messages. */ +data class RadioSourceMessage ( + val url: String, + val title: String? = null, + val artwork: String? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): RadioSourceMessage { + val url = pigeonVar_list[0] as String + val title = pigeonVar_list[1] as String? + val artwork = pigeonVar_list[2] as String? + return RadioSourceMessage(url, title, artwork) + } + } + fun toList(): List { + return listOf( + url, + title, + artwork, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is RadioSourceMessage) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class NowPlayingInfoMessage ( + val title: String? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): NowPlayingInfoMessage { + val title = pigeonVar_list[0] as String? + return NowPlayingInfoMessage(title) + } + } + fun toList(): List { + return listOf( + title, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is NowPlayingInfoMessage) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class VolumeInfoMessage ( + val volume: Double, + val isMuted: Boolean +) + { + companion object { + fun fromList(pigeonVar_list: List): VolumeInfoMessage { + val volume = pigeonVar_list[0] as Double + val isMuted = pigeonVar_list[1] as Boolean + return VolumeInfoMessage(volume, isMuted) + } + } + fun toList(): List { + return listOf( + volume, + isMuted, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is VolumeInfoMessage) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} +private open class MessagesPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as? List)?.let { + RadioSourceMessage.fromList(it) + } + } + 130.toByte() -> { + return (readValue(buffer) as? List)?.let { + NowPlayingInfoMessage.fromList(it) + } + } + 131.toByte() -> { + return (readValue(buffer) as? List)?.let { + VolumeInfoMessage.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is RadioSourceMessage -> { + stream.write(129) + writeValue(stream, value.toList()) + } + is NowPlayingInfoMessage -> { + stream.write(130) + writeValue(stream, value.toList()) + } + is VolumeInfoMessage -> { + stream.write(131) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + +val MessagesPigeonMethodCodec = StandardMethodCodec(MessagesPigeonCodec()) + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface RadioPlayerHostApi { + fun initialize(sources: List, playWhenReady: Boolean) + fun play() + fun pause() + fun playOrPause() + fun setVolume(volume: Double) + fun getVolume(): Double + fun nextSource() + fun previousSource() + fun jumpToSourceAtIndex(index: Long) + fun dispose() + + companion object { + /** The codec used by RadioPlayerHostApi. */ + val codec: MessageCodec by lazy { + MessagesPigeonCodec() + } + /** Sets up an instance of `RadioPlayerHostApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: RadioPlayerHostApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerHostApi.initialize$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val sourcesArg = args[0] as List + val playWhenReadyArg = args[1] as Boolean + val wrapped: List = try { + api.initialize(sourcesArg, playWhenReadyArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerHostApi.play$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.play() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerHostApi.pause$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.pause() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerHostApi.playOrPause$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.playOrPause() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerHostApi.setVolume$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val volumeArg = args[0] as Double + val wrapped: List = try { + api.setVolume(volumeArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerHostApi.getVolume$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.getVolume()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerHostApi.nextSource$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.nextSource() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerHostApi.previousSource$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.previousSource() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerHostApi.jumpToSourceAtIndex$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val indexArg = args[0] as Long + val wrapped: List = try { + api.jumpToSourceAtIndex(indexArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerHostApi.dispose$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.dispose() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} + +private class MessagesPigeonStreamHandler( + val wrapper: MessagesPigeonEventChannelWrapper +) : EventChannel.StreamHandler { + var pigeonSink: PigeonEventSink? = null + + override fun onListen(p0: Any?, sink: EventChannel.EventSink) { + pigeonSink = PigeonEventSink(sink) + wrapper.onListen(p0, pigeonSink!!) + } + + override fun onCancel(p0: Any?) { + pigeonSink = null + wrapper.onCancel(p0) + } +} + +interface MessagesPigeonEventChannelWrapper { + open fun onListen(p0: Any?, sink: PigeonEventSink) {} + + open fun onCancel(p0: Any?) {} +} + +class PigeonEventSink(private val sink: EventChannel.EventSink) { + fun success(value: T) { + sink.success(value) + } + + fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + sink.error(errorCode, errorMessage, errorDetails) + } + + fun endOfStream() { + sink.endOfStream() + } +} + +abstract class OnPlaybackStateChangedStreamHandler : MessagesPigeonEventChannelWrapper { + companion object { + fun register(messenger: BinaryMessenger, streamHandler: OnPlaybackStateChangedStreamHandler, instanceName: String = "") { + var channelName: String = "dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerEventApi.onPlaybackStateChanged" + if (instanceName.isNotEmpty()) { + channelName += ".$instanceName" + } + val internalStreamHandler = MessagesPigeonStreamHandler(streamHandler) + EventChannel(messenger, channelName, MessagesPigeonMethodCodec).setStreamHandler(internalStreamHandler) + } + } +// Implement methods from MessagesPigeonEventChannelWrapper +override fun onListen(p0: Any?, sink: PigeonEventSink) {} + +override fun onCancel(p0: Any?) {} +} + +abstract class OnNowPlayingChangedStreamHandler : MessagesPigeonEventChannelWrapper { + companion object { + fun register(messenger: BinaryMessenger, streamHandler: OnNowPlayingChangedStreamHandler, instanceName: String = "") { + var channelName: String = "dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerEventApi.onNowPlayingChanged" + if (instanceName.isNotEmpty()) { + channelName += ".$instanceName" + } + val internalStreamHandler = MessagesPigeonStreamHandler(streamHandler) + EventChannel(messenger, channelName, MessagesPigeonMethodCodec).setStreamHandler(internalStreamHandler) + } + } +// Implement methods from MessagesPigeonEventChannelWrapper +override fun onListen(p0: Any?, sink: PigeonEventSink) {} + +override fun onCancel(p0: Any?) {} +} + +abstract class OnVolumeChangedStreamHandler : MessagesPigeonEventChannelWrapper { + companion object { + fun register(messenger: BinaryMessenger, streamHandler: OnVolumeChangedStreamHandler, instanceName: String = "") { + var channelName: String = "dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerEventApi.onVolumeChanged" + if (instanceName.isNotEmpty()) { + channelName += ".$instanceName" + } + val internalStreamHandler = MessagesPigeonStreamHandler(streamHandler) + EventChannel(messenger, channelName, MessagesPigeonMethodCodec).setStreamHandler(internalStreamHandler) + } + } +// Implement methods from MessagesPigeonEventChannelWrapper +override fun onListen(p0: Any?, sink: PigeonEventSink) {} + +override fun onCancel(p0: Any?) {} +} + diff --git a/packages/flutter_radio_player_android/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/PlaybackService.kt b/packages/flutter_radio_player_android/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/PlaybackService.kt new file mode 100644 index 0000000..cb37a75 --- /dev/null +++ b/packages/flutter_radio_player_android/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/PlaybackService.kt @@ -0,0 +1,127 @@ +package me.sithiramunasinghe.flutter.flutter_radio_player.core + +import android.content.Intent +import android.os.Handler +import android.os.Looper +import androidx.annotation.OptIn +import androidx.media3.common.AudioAttributes +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import androidx.media3.common.Metadata +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.extractor.metadata.icy.IcyInfo +import androidx.media3.session.DefaultMediaNotificationProvider +import androidx.media3.session.MediaLibraryService +import androidx.media3.session.MediaSession +import androidx.media3.session.MediaSession.ControllerInfo +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture +import me.sithiramunasinghe.flutter.flutter_radio_player.FlutterRadioPlayerPlugin +import me.sithiramunasinghe.flutter.flutter_radio_player.NowPlayingInfoMessage +import me.sithiramunasinghe.flutter.flutter_radio_player.PigeonEventSink +import me.sithiramunasinghe.flutter.flutter_radio_player.VolumeInfoMessage + +class PlaybackService : MediaLibraryService() { + + private lateinit var player: Player + private var mediaSession: MediaLibrarySession? = null + private val mainHandler = Handler(Looper.getMainLooper()) + + companion object { + var latestMetadata: MediaMetadata? = null + var playbackStateSink: PigeonEventSink? = null + var nowPlayingSink: PigeonEventSink? = null + var volumeSink: PigeonEventSink? = null + } + + override fun onCreate() { + super.onCreate() + initializeSessionAndPlayer() + } + + override fun onDestroy() { + mediaSession?.run { + player.release() + release() + } + mediaSession = null + super.onDestroy() + } + + override fun onTaskRemoved(rootIntent: Intent?) { + if (mediaSession?.player != null) { + stopSelf() + } + } + + override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession? = mediaSession + + @OptIn(UnstableApi::class) + private fun initializeSessionAndPlayer() { + player = ExoPlayer.Builder(this) + .setAudioAttributes(AudioAttributes.DEFAULT, true) + .build() + + mediaSession = MediaLibrarySession.Builder(this, player, object : MediaLibrarySession.Callback { + override fun onAddMediaItems( + mediaSession: MediaSession, + controller: ControllerInfo, + mediaItems: MutableList + ): ListenableFuture> { + return Futures.immediateFuture(mediaItems) + } + }) + .setSessionActivity(FlutterRadioPlayerPlugin.sessionActivity) + .build() + + val notificationProvider = DefaultMediaNotificationProvider(this) + val appInfo = packageManager.getApplicationInfo(packageName, 0) + notificationProvider.setSmallIcon(appInfo.icon) + setMediaNotificationProvider(notificationProvider) + + player.addListener(object : Player.Listener { + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + mainHandler.post { nowPlayingSink?.success(NowPlayingInfoMessage(title = null)) } + } + + override fun onIsPlayingChanged(isPlaying: Boolean) { + mainHandler.post { playbackStateSink?.success(isPlaying) } + } + + override fun onVolumeChanged(volume: Float) { + mainHandler.post { + volumeSink?.success(VolumeInfoMessage(volume = volume.toDouble(), isMuted = false)) + } + } + + override fun onMetadata(metadata: Metadata) { + for (i in 0 until metadata.length()) { + val entry = metadata[i] + if (entry is IcyInfo && !entry.title.isNullOrEmpty()) { + mainHandler.post { + nowPlayingSink?.success(NowPlayingInfoMessage(title = entry.title)) + } + latestMetadata = null + return + } + } + } + + override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) { + latestMetadata = mediaMetadata + val title = mediaMetadata.title?.toString() ?: return + mainHandler.post { + nowPlayingSink?.success(NowPlayingInfoMessage(title = title)) + } + } + + override fun onPlaybackStateChanged(playbackState: Int) { + if (playbackState == Player.STATE_READY) { + mainHandler.post { playbackStateSink?.success(false) } + } + } + }) + } +} diff --git a/packages/flutter_radio_player_android/lib/flutter_radio_player_android.dart b/packages/flutter_radio_player_android/lib/flutter_radio_player_android.dart new file mode 100644 index 0000000..afa9714 --- /dev/null +++ b/packages/flutter_radio_player_android/lib/flutter_radio_player_android.dart @@ -0,0 +1,71 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_radio_player_platform_interface/flutter_radio_player_platform_interface.dart'; + +import 'src/messages.g.dart'; + +class FlutterRadioPlayerAndroid extends FlutterRadioPlayerPlatform { + final RadioPlayerHostApi _hostApi = RadioPlayerHostApi(); + + static void registerWith() { + FlutterRadioPlayerPlatform.instance = FlutterRadioPlayerAndroid(); + } + + @override + Future initialize( + List sources, { + bool playWhenReady = false, + }) { + return _hostApi.initialize( + sources.map(_toMessage).toList(), + playWhenReady, + ); + } + + @override + Future play() => _hostApi.play(); + + @override + Future pause() => _hostApi.pause(); + + @override + Future playOrPause() => _hostApi.playOrPause(); + + @override + Future setVolume(double volume) => _hostApi.setVolume(volume); + + @override + Future getVolume() => _hostApi.getVolume(); + + @override + Future nextSource() => _hostApi.nextSource(); + + @override + Future previousSource() => _hostApi.previousSource(); + + @override + Future jumpToSourceAtIndex(int index) => + _hostApi.jumpToSourceAtIndex(index); + + @override + Future dispose() => _hostApi.dispose(); + + @override + @visibleForTesting + Stream get isPlayingStream => onPlaybackStateChanged(); + + @override + Stream get nowPlayingStream => + onNowPlayingChanged().map((msg) => NowPlayingInfo(title: msg.title)); + + @override + Stream get volumeStream => onVolumeChanged() + .map((msg) => VolumeInfo(volume: msg.volume, isMuted: msg.isMuted)); + + static RadioSourceMessage _toMessage(RadioSource source) { + return RadioSourceMessage( + url: source.url, + title: source.title, + artwork: source.artwork, + ); + } +} diff --git a/packages/flutter_radio_player_android/lib/src/messages.g.dart b/packages/flutter_radio_player_android/lib/src/messages.g.dart new file mode 100644 index 0000000..17ccde5 --- /dev/null +++ b/packages/flutter_radio_player_android/lib/src/messages.g.dart @@ -0,0 +1,480 @@ +// Autogenerated from Pigeon (v26.1.7), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, omit_obvious_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} +bool _deepEquals(Object? a, Object? b) { + if (a is List && b is List) { + return a.length == b.length && + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + } + if (a is Map && b is Map) { + return a.length == b.length && a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); + } + return a == b; +} + + +class RadioSourceMessage { + RadioSourceMessage({ + required this.url, + this.title, + this.artwork, + }); + + String url; + + String? title; + + String? artwork; + + List _toList() { + return [ + url, + title, + artwork, + ]; + } + + Object encode() { + return _toList(); } + + static RadioSourceMessage decode(Object result) { + result as List; + return RadioSourceMessage( + url: result[0]! as String, + title: result[1] as String?, + artwork: result[2] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! RadioSourceMessage || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + +class NowPlayingInfoMessage { + NowPlayingInfoMessage({ + this.title, + }); + + String? title; + + List _toList() { + return [ + title, + ]; + } + + Object encode() { + return _toList(); } + + static NowPlayingInfoMessage decode(Object result) { + result as List; + return NowPlayingInfoMessage( + title: result[0] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! NowPlayingInfoMessage || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + +class VolumeInfoMessage { + VolumeInfoMessage({ + required this.volume, + required this.isMuted, + }); + + double volume; + + bool isMuted; + + List _toList() { + return [ + volume, + isMuted, + ]; + } + + Object encode() { + return _toList(); } + + static VolumeInfoMessage decode(Object result) { + result as List; + return VolumeInfoMessage( + volume: result[0]! as double, + isMuted: result[1]! as bool, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! VolumeInfoMessage || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is RadioSourceMessage) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is NowPlayingInfoMessage) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is VolumeInfoMessage) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + return RadioSourceMessage.decode(readValue(buffer)!); + case 130: + return NowPlayingInfoMessage.decode(readValue(buffer)!); + case 131: + return VolumeInfoMessage.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +const StandardMethodCodec pigeonMethodCodec = StandardMethodCodec(_PigeonCodec()); + +class RadioPlayerHostApi { + /// Constructor for [RadioPlayerHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + RadioPlayerHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future initialize(List sources, bool playWhenReady) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerHostApi.initialize$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([sources, playWhenReady]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future play() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerHostApi.play$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future pause() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerHostApi.pause$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future playOrPause() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerHostApi.playOrPause$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future setVolume(double volume) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerHostApi.setVolume$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([volume]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future getVolume() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerHostApi.getVolume$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as double?)!; + } + } + + Future nextSource() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerHostApi.nextSource$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future previousSource() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerHostApi.previousSource$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future jumpToSourceAtIndex(int index) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerHostApi.jumpToSourceAtIndex$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([index]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future dispose() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerHostApi.dispose$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } +} + +Stream onPlaybackStateChanged( {String instanceName = ''}) { + if (instanceName.isNotEmpty) { + instanceName = '.$instanceName'; + } + final EventChannel onPlaybackStateChangedChannel = + EventChannel('dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerEventApi.onPlaybackStateChanged$instanceName', pigeonMethodCodec); + return onPlaybackStateChangedChannel.receiveBroadcastStream().map((dynamic event) { + return event as bool; + }); +} + +Stream onNowPlayingChanged( {String instanceName = ''}) { + if (instanceName.isNotEmpty) { + instanceName = '.$instanceName'; + } + final EventChannel onNowPlayingChangedChannel = + EventChannel('dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerEventApi.onNowPlayingChanged$instanceName', pigeonMethodCodec); + return onNowPlayingChangedChannel.receiveBroadcastStream().map((dynamic event) { + return event as NowPlayingInfoMessage; + }); +} + +Stream onVolumeChanged( {String instanceName = ''}) { + if (instanceName.isNotEmpty) { + instanceName = '.$instanceName'; + } + final EventChannel onVolumeChangedChannel = + EventChannel('dev.flutter.pigeon.flutter_radio_player_android.RadioPlayerEventApi.onVolumeChanged$instanceName', pigeonMethodCodec); + return onVolumeChangedChannel.receiveBroadcastStream().map((dynamic event) { + return event as VolumeInfoMessage; + }); +} + diff --git a/packages/flutter_radio_player_android/pigeons/messages.dart b/packages/flutter_radio_player_android/pigeons/messages.dart new file mode 100644 index 0000000..ce37708 --- /dev/null +++ b/packages/flutter_radio_player_android/pigeons/messages.dart @@ -0,0 +1,45 @@ +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + kotlinOut: + 'android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/Messages.g.kt', + kotlinOptions: KotlinOptions( + package: 'me.sithiramunasinghe.flutter.flutter_radio_player', + ), +)) +class RadioSourceMessage { + late String url; + late String? title; + late String? artwork; +} + +class NowPlayingInfoMessage { + late String? title; +} + +class VolumeInfoMessage { + late double volume; + late bool isMuted; +} + +@HostApi() +abstract class RadioPlayerHostApi { + void initialize(List sources, bool playWhenReady); + void play(); + void pause(); + void playOrPause(); + void setVolume(double volume); + double getVolume(); + void nextSource(); + void previousSource(); + void jumpToSourceAtIndex(int index); + void dispose(); +} + +@EventChannelApi() +abstract class RadioPlayerEventApi { + bool onPlaybackStateChanged(); + NowPlayingInfoMessage onNowPlayingChanged(); + VolumeInfoMessage onVolumeChanged(); +} diff --git a/packages/flutter_radio_player_android/pubspec.lock b/packages/flutter_radio_player_android/pubspec.lock new file mode 100644 index 0000000..6b9a915 --- /dev/null +++ b/packages/flutter_radio_player_android/pubspec.lock @@ -0,0 +1,372 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "5b7468c326d2f8a4f630056404ca0d291ade42918f4a3c6233618e724f39da8e" + url: "https://pub.dev" + source: hosted + version: "92.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "70e4b1ef8003c64793a9e268a551a82869a8a96f39deb73dea28084b0e8bf75e" + url: "https://pub.dev" + source: hosted + version: "9.0.0" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "7931c90b84bc573fef103548e354258ae4c9d28d140e41961df6843c5d60d4d8" + url: "https://pub.dev" + source: hosted + version: "8.12.3" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + url: "https://pub.dev" + source: hosted + version: "4.11.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b + url: "https://pub.dev" + source: hosted + version: "3.1.3" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + flutter_radio_player_platform_interface: + dependency: "direct main" + description: + path: "../flutter_radio_player_platform_interface" + relative: true + source: path + version: "4.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pigeon: + dependency: "direct dev" + description: + name: pigeon + sha256: b12f739047654cd65338dc7e9c6c03cbd4a1de179364a45c9e8bbeccf23729c6 + url: "https://pub.dev" + source: hosted + version: "26.1.7" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/packages/flutter_radio_player_android/pubspec.yaml b/packages/flutter_radio_player_android/pubspec.yaml new file mode 100644 index 0000000..c485969 --- /dev/null +++ b/packages/flutter_radio_player_android/pubspec.yaml @@ -0,0 +1,29 @@ +name: flutter_radio_player_android +description: Android implementation of flutter_radio_player. +version: 4.0.0 +homepage: https://github.com/Sithira/FlutterRadioPlayer + +environment: + sdk: '>=3.4.3 <4.0.0' + flutter: '>=3.3.0' + +flutter: + plugin: + implements: flutter_radio_player + platforms: + android: + dartPluginClass: FlutterRadioPlayerAndroid + package: me.sithiramunasinghe.flutter.flutter_radio_player + pluginClass: FlutterRadioPlayerPlugin + +dependencies: + flutter: + sdk: flutter + flutter_radio_player_platform_interface: + path: ../flutter_radio_player_platform_interface + +dev_dependencies: + pigeon: ^26.0.0 + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 diff --git a/packages/flutter_radio_player_android/test/flutter_radio_player_android_test.dart b/packages/flutter_radio_player_android/test/flutter_radio_player_android_test.dart new file mode 100644 index 0000000..f2960b4 --- /dev/null +++ b/packages/flutter_radio_player_android/test/flutter_radio_player_android_test.dart @@ -0,0 +1,13 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_radio_player_android/flutter_radio_player_android.dart'; +import 'package:flutter_radio_player_platform_interface/flutter_radio_player_platform_interface.dart'; + +void main() { + test('registerWith sets platform instance', () { + FlutterRadioPlayerAndroid.registerWith(); + expect( + FlutterRadioPlayerPlatform.instance, + isA(), + ); + }); +} diff --git a/packages/flutter_radio_player_ios/analysis_options.yaml b/packages/flutter_radio_player_ios/analysis_options.yaml new file mode 100644 index 0000000..ed7644f --- /dev/null +++ b/packages/flutter_radio_player_ios/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + - "lib/src/messages.g.dart" diff --git a/packages/flutter_radio_player_ios/ios/flutter_radio_player_ios.podspec b/packages/flutter_radio_player_ios/ios/flutter_radio_player_ios.podspec new file mode 100644 index 0000000..2093a76 --- /dev/null +++ b/packages/flutter_radio_player_ios/ios/flutter_radio_player_ios.podspec @@ -0,0 +1,18 @@ +Pod::Spec.new do |s| + s.name = 'flutter_radio_player_ios' + s.version = '4.0.0' + s.summary = 'iOS implementation of flutter_radio_player.' + s.description = <<-DESC +iOS implementation of the flutter_radio_player plugin using AVFoundation. + DESC + s.homepage = 'https://github.com/Sithira/FlutterRadioPlayer' + s.license = { :file => '../LICENSE' } + s.author = { 'Sithira Munasinghe' => 'sithira@gmail.com' } + s.source = { :path => '.' } + s.source_files = 'flutter_radio_player_ios/Sources/flutter_radio_player_ios/**/*.swift' + s.dependency 'Flutter' + s.platform = :ios, '14.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' + s.resource_bundles = {'flutter_radio_player_ios_privacy' => ['flutter_radio_player_ios/Sources/flutter_radio_player_ios/PrivacyInfo.xcprivacy']} +end diff --git a/packages/flutter_radio_player_ios/ios/flutter_radio_player_ios/Sources/flutter_radio_player_ios/FlutterRadioPlayerPlugin.swift b/packages/flutter_radio_player_ios/ios/flutter_radio_player_ios/Sources/flutter_radio_player_ios/FlutterRadioPlayerPlugin.swift new file mode 100644 index 0000000..a3a7f84 --- /dev/null +++ b/packages/flutter_radio_player_ios/ios/flutter_radio_player_ios/Sources/flutter_radio_player_ios/FlutterRadioPlayerPlugin.swift @@ -0,0 +1,107 @@ +import Flutter +import UIKit + +public class FlutterRadioPlayerPlugin: NSObject, FlutterPlugin, RadioPlayerHostApi { + private let service = RadioPlayerService.instance + + public static func register(with registrar: FlutterPluginRegistrar) { + let instance = FlutterRadioPlayerPlugin() + RadioPlayerService.instance.setRegistrar(registrar) + RadioPlayerHostApiSetup.setUp(binaryMessenger: registrar.messenger(), api: instance) + instance.setupEventChannels(messenger: registrar.messenger()) + } + + func initialize(sources: [RadioSourceMessage], playWhenReady: Bool) throws { + service.initialize(sources: sources, playWhenReady: playWhenReady) + } + + func play() throws { + service.play() + } + + func pause() throws { + service.pause() + } + + func playOrPause() throws { + service.playOrPause() + } + + func setVolume(volume: Double) throws { + service.setVolume(volume: Float(volume)) + } + + func getVolume() throws -> Double { + return Double(service.getVolume()) + } + + func nextSource() throws { + service.nextSource() + } + + func previousSource() throws { + service.previousSource() + } + + func jumpToSourceAtIndex(index: Int64) throws { + service.jumpToSourceAtIndex(index: Int(index)) + } + + func dispose() throws { + service.dispose() + } + + private func setupEventChannels(messenger: FlutterBinaryMessenger) { + OnPlaybackStateChangedStreamHandler.register( + with: messenger, + streamHandler: PlaybackStateStreamWrapper(service: service) + ) + OnNowPlayingChangedStreamHandler.register( + with: messenger, + streamHandler: NowPlayingStreamWrapper(service: service) + ) + OnVolumeChangedStreamHandler.register( + with: messenger, + streamHandler: VolumeStreamWrapper(service: service) + ) + } +} + +private class PlaybackStateStreamWrapper: OnPlaybackStateChangedStreamHandler { + let service: RadioPlayerService + init(service: RadioPlayerService) { self.service = service } + + override func onListen(withArguments arguments: Any?, sink: PigeonEventSink) { + service.playbackStateSink = sink + } + + override func onCancel(withArguments arguments: Any?) { + service.playbackStateSink = nil + } +} + +private class NowPlayingStreamWrapper: OnNowPlayingChangedStreamHandler { + let service: RadioPlayerService + init(service: RadioPlayerService) { self.service = service } + + override func onListen(withArguments arguments: Any?, sink: PigeonEventSink) { + service.nowPlayingSink = sink + } + + override func onCancel(withArguments arguments: Any?) { + service.nowPlayingSink = nil + } +} + +private class VolumeStreamWrapper: OnVolumeChangedStreamHandler { + let service: RadioPlayerService + init(service: RadioPlayerService) { self.service = service } + + override func onListen(withArguments arguments: Any?, sink: PigeonEventSink) { + service.volumeSink = sink + } + + override func onCancel(withArguments arguments: Any?) { + service.volumeSink = nil + } +} diff --git a/packages/flutter_radio_player_ios/ios/flutter_radio_player_ios/Sources/flutter_radio_player_ios/Messages.g.swift b/packages/flutter_radio_player_ios/ios/flutter_radio_player_ios/Sources/flutter_radio_player_ios/Messages.g.swift new file mode 100644 index 0000000..d8c96e5 --- /dev/null +++ b/packages/flutter_radio_player_ios/ios/flutter_radio_player_ios/Sources/flutter_radio_player_ios/Messages.g.swift @@ -0,0 +1,516 @@ +// Autogenerated from Pigeon (v26.1.7), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +/// Error class for passing custom error details to Dart side. +final class PigeonError: Error { + let code: String + let message: String? + let details: Sendable? + + init(code: String, message: String?, details: Sendable?) { + self.code = code + self.message = message + self.details = details + } + + var localizedDescription: String { + return + "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + +func deepEqualsMessages(_ lhs: Any?, _ rhs: Any?) -> Bool { + let cleanLhs = nilOrValue(lhs) as Any? + let cleanRhs = nilOrValue(rhs) as Any? + switch (cleanLhs, cleanRhs) { + case (nil, nil): + return true + + case (nil, _), (_, nil): + return false + + case is (Void, Void): + return true + + case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable): + return cleanLhsHashable == cleanRhsHashable + + case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]): + guard cleanLhsArray.count == cleanRhsArray.count else { return false } + for (index, element) in cleanLhsArray.enumerated() { + if !deepEqualsMessages(element, cleanRhsArray[index]) { + return false + } + } + return true + + case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false } + for (key, cleanLhsValue) in cleanLhsDictionary { + guard cleanRhsDictionary.index(forKey: key) != nil else { return false } + if !deepEqualsMessages(cleanLhsValue, cleanRhsDictionary[key]!) { + return false + } + } + return true + + default: + // Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue. + return false + } +} + +func deepHashMessages(value: Any?, hasher: inout Hasher) { + if let valueList = value as? [AnyHashable] { + for item in valueList { deepHashMessages(value: item, hasher: &hasher) } + return + } + + if let valueDict = value as? [AnyHashable: AnyHashable] { + for key in valueDict.keys { + hasher.combine(key) + deepHashMessages(value: valueDict[key]!, hasher: &hasher) + } + return + } + + if let hashableValue = value as? AnyHashable { + hasher.combine(hashableValue.hashValue) + } + + return hasher.combine(String(describing: value)) +} + + + +/// Generated class from Pigeon that represents data sent in messages. +struct RadioSourceMessage: Hashable { + var url: String + var title: String? = nil + var artwork: String? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> RadioSourceMessage? { + let url = pigeonVar_list[0] as! String + let title: String? = nilOrValue(pigeonVar_list[1]) + let artwork: String? = nilOrValue(pigeonVar_list[2]) + + return RadioSourceMessage( + url: url, + title: title, + artwork: artwork + ) + } + func toList() -> [Any?] { + return [ + url, + title, + artwork, + ] + } + static func == (lhs: RadioSourceMessage, rhs: RadioSourceMessage) -> Bool { + return deepEqualsMessages(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashMessages(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct NowPlayingInfoMessage: Hashable { + var title: String? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> NowPlayingInfoMessage? { + let title: String? = nilOrValue(pigeonVar_list[0]) + + return NowPlayingInfoMessage( + title: title + ) + } + func toList() -> [Any?] { + return [ + title + ] + } + static func == (lhs: NowPlayingInfoMessage, rhs: NowPlayingInfoMessage) -> Bool { + return deepEqualsMessages(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashMessages(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct VolumeInfoMessage: Hashable { + var volume: Double + var isMuted: Bool + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> VolumeInfoMessage? { + let volume = pigeonVar_list[0] as! Double + let isMuted = pigeonVar_list[1] as! Bool + + return VolumeInfoMessage( + volume: volume, + isMuted: isMuted + ) + } + func toList() -> [Any?] { + return [ + volume, + isMuted, + ] + } + static func == (lhs: VolumeInfoMessage, rhs: VolumeInfoMessage) -> Bool { + return deepEqualsMessages(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashMessages(value: toList(), hasher: &hasher) + } +} + +private class MessagesPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + return RadioSourceMessage.fromList(self.readValue() as! [Any?]) + case 130: + return NowPlayingInfoMessage.fromList(self.readValue() as! [Any?]) + case 131: + return VolumeInfoMessage.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class MessagesPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? RadioSourceMessage { + super.writeByte(129) + super.writeValue(value.toList()) + } else if let value = value as? NowPlayingInfoMessage { + super.writeByte(130) + super.writeValue(value.toList()) + } else if let value = value as? VolumeInfoMessage { + super.writeByte(131) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class MessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return MessagesPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return MessagesPigeonCodecWriter(data: data) + } +} + +class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter()) +} + +var messagesPigeonMethodCodec = FlutterStandardMethodCodec(readerWriter: MessagesPigeonCodecReaderWriter()); + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol RadioPlayerHostApi { + func initialize(sources: [RadioSourceMessage], playWhenReady: Bool) throws + func play() throws + func pause() throws + func playOrPause() throws + func setVolume(volume: Double) throws + func getVolume() throws -> Double + func nextSource() throws + func previousSource() throws + func jumpToSourceAtIndex(index: Int64) throws + func dispose() throws +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class RadioPlayerHostApiSetup { + static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared } + /// Sets up an instance of `RadioPlayerHostApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: RadioPlayerHostApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let initializeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerHostApi.initialize\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + initializeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let sourcesArg = args[0] as! [RadioSourceMessage] + let playWhenReadyArg = args[1] as! Bool + do { + try api.initialize(sources: sourcesArg, playWhenReady: playWhenReadyArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + initializeChannel.setMessageHandler(nil) + } + let playChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerHostApi.play\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + playChannel.setMessageHandler { _, reply in + do { + try api.play() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + playChannel.setMessageHandler(nil) + } + let pauseChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerHostApi.pause\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + pauseChannel.setMessageHandler { _, reply in + do { + try api.pause() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + pauseChannel.setMessageHandler(nil) + } + let playOrPauseChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerHostApi.playOrPause\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + playOrPauseChannel.setMessageHandler { _, reply in + do { + try api.playOrPause() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + playOrPauseChannel.setMessageHandler(nil) + } + let setVolumeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerHostApi.setVolume\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setVolumeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let volumeArg = args[0] as! Double + do { + try api.setVolume(volume: volumeArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + setVolumeChannel.setMessageHandler(nil) + } + let getVolumeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerHostApi.getVolume\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getVolumeChannel.setMessageHandler { _, reply in + do { + let result = try api.getVolume() + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getVolumeChannel.setMessageHandler(nil) + } + let nextSourceChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerHostApi.nextSource\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + nextSourceChannel.setMessageHandler { _, reply in + do { + try api.nextSource() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + nextSourceChannel.setMessageHandler(nil) + } + let previousSourceChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerHostApi.previousSource\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + previousSourceChannel.setMessageHandler { _, reply in + do { + try api.previousSource() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + previousSourceChannel.setMessageHandler(nil) + } + let jumpToSourceAtIndexChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerHostApi.jumpToSourceAtIndex\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + jumpToSourceAtIndexChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let indexArg = args[0] as! Int64 + do { + try api.jumpToSourceAtIndex(index: indexArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + jumpToSourceAtIndexChannel.setMessageHandler(nil) + } + let disposeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerHostApi.dispose\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + disposeChannel.setMessageHandler { _, reply in + do { + try api.dispose() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + disposeChannel.setMessageHandler(nil) + } + } +} + +private class PigeonStreamHandler: NSObject, FlutterStreamHandler { + private let wrapper: PigeonEventChannelWrapper + private var pigeonSink: PigeonEventSink? = nil + + init(wrapper: PigeonEventChannelWrapper) { + self.wrapper = wrapper + } + + func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) + -> FlutterError? + { + pigeonSink = PigeonEventSink(events) + wrapper.onListen(withArguments: arguments, sink: pigeonSink!) + return nil + } + + func onCancel(withArguments arguments: Any?) -> FlutterError? { + pigeonSink = nil + wrapper.onCancel(withArguments: arguments) + return nil + } +} + +class PigeonEventChannelWrapper { + func onListen(withArguments arguments: Any?, sink: PigeonEventSink) {} + func onCancel(withArguments arguments: Any?) {} +} + +class PigeonEventSink { + private let sink: FlutterEventSink + + init(_ sink: @escaping FlutterEventSink) { + self.sink = sink + } + + func success(_ value: ReturnType) { + sink(value) + } + + func error(code: String, message: String?, details: Any?) { + sink(FlutterError(code: code, message: message, details: details)) + } + + func endOfStream() { + sink(FlutterEndOfEventStream) + } + +} + +class OnPlaybackStateChangedStreamHandler: PigeonEventChannelWrapper { + static func register(with messenger: FlutterBinaryMessenger, + instanceName: String = "", + streamHandler: OnPlaybackStateChangedStreamHandler) { + var channelName = "dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerEventApi.onPlaybackStateChanged" + if !instanceName.isEmpty { + channelName += ".\(instanceName)" + } + let internalStreamHandler = PigeonStreamHandler(wrapper: streamHandler) + let channel = FlutterEventChannel(name: channelName, binaryMessenger: messenger, codec: messagesPigeonMethodCodec) + channel.setStreamHandler(internalStreamHandler) + } +} + +class OnNowPlayingChangedStreamHandler: PigeonEventChannelWrapper { + static func register(with messenger: FlutterBinaryMessenger, + instanceName: String = "", + streamHandler: OnNowPlayingChangedStreamHandler) { + var channelName = "dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerEventApi.onNowPlayingChanged" + if !instanceName.isEmpty { + channelName += ".\(instanceName)" + } + let internalStreamHandler = PigeonStreamHandler(wrapper: streamHandler) + let channel = FlutterEventChannel(name: channelName, binaryMessenger: messenger, codec: messagesPigeonMethodCodec) + channel.setStreamHandler(internalStreamHandler) + } +} + +class OnVolumeChangedStreamHandler: PigeonEventChannelWrapper { + static func register(with messenger: FlutterBinaryMessenger, + instanceName: String = "", + streamHandler: OnVolumeChangedStreamHandler) { + var channelName = "dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerEventApi.onVolumeChanged" + if !instanceName.isEmpty { + channelName += ".\(instanceName)" + } + let internalStreamHandler = PigeonStreamHandler(wrapper: streamHandler) + let channel = FlutterEventChannel(name: channelName, binaryMessenger: messenger, codec: messagesPigeonMethodCodec) + channel.setStreamHandler(internalStreamHandler) + } +} + diff --git a/packages/flutter_radio_player_ios/ios/flutter_radio_player_ios/Sources/flutter_radio_player_ios/PrivacyInfo.xcprivacy b/packages/flutter_radio_player_ios/ios/flutter_radio_player_ios/Sources/flutter_radio_player_ios/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..d37d627 --- /dev/null +++ b/packages/flutter_radio_player_ios/ios/flutter_radio_player_ios/Sources/flutter_radio_player_ios/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyCollectedDataTypes + + NSPrivacyAccessedAPITypes + + NSPrivacyTrackingDomains + + NSPrivacyTracking + + + diff --git a/packages/flutter_radio_player_ios/ios/flutter_radio_player_ios/Sources/flutter_radio_player_ios/core/RadioPlayerService.swift b/packages/flutter_radio_player_ios/ios/flutter_radio_player_ios/Sources/flutter_radio_player_ios/core/RadioPlayerService.swift new file mode 100644 index 0000000..525e918 --- /dev/null +++ b/packages/flutter_radio_player_ios/ios/flutter_radio_player_ios/Sources/flutter_radio_player_ios/core/RadioPlayerService.swift @@ -0,0 +1,323 @@ +import Flutter +import Foundation +import AVFoundation +import MediaPlayer +import UIKit + +class RadioPlayerService: NSObject { + static let instance = RadioPlayerService() + + private var player: AVPlayer? + private var sources: [RadioSourceMessage] = [] + private var currentIndex: Int = 0 + private var timeControlStatusObservation: NSKeyValueObservation? + private var statusObservation: NSKeyValueObservation? + private var metadataOutput: AVPlayerItemMetadataOutput? + + var playbackStateSink: PigeonEventSink? + var nowPlayingSink: PigeonEventSink? + var volumeSink: PigeonEventSink? + + private var registrar: FlutterPluginRegistrar? + + private override init() { + super.init() + setupAudioSession() + setupRemoteCommands() + player = AVPlayer() + player?.volume = 0.5 + observePlayerState() + observeInterruptions() + observeRouteChanges() + } + + func setRegistrar(_ registrar: FlutterPluginRegistrar) { + self.registrar = registrar + } + + func initialize(sources: [RadioSourceMessage], playWhenReady: Bool) { + self.sources = sources + self.currentIndex = 0 + + guard !sources.isEmpty else { return } + + loadSource(at: 0) + if playWhenReady { + player?.play() + } + } + + func play() { + try? AVAudioSession.sharedInstance().setActive(true) + player?.play() + } + + func pause() { + player?.pause() + } + + func playOrPause() { + if player?.timeControlStatus == .playing { + pause() + } else { + play() + } + } + + func setVolume(volume: Float) { + player?.volume = volume + DispatchQueue.main.async { + self.volumeSink?.success(VolumeInfoMessage(volume: Double(volume), isMuted: volume == 0)) + } + } + + func getVolume() -> Float { + return player?.volume ?? 0.5 + } + + func nextSource() { + guard !sources.isEmpty else { return } + let nextIndex = (currentIndex + 1) % sources.count + loadSource(at: nextIndex) + player?.play() + } + + func previousSource() { + guard !sources.isEmpty else { return } + let prevIndex = (currentIndex - 1 + sources.count) % sources.count + loadSource(at: prevIndex) + player?.play() + } + + func jumpToSourceAtIndex(index: Int) { + guard index >= 0 && index < sources.count else { return } + loadSource(at: index) + player?.play() + } + + func dispose() { + player?.pause() + player?.replaceCurrentItem(with: nil) + timeControlStatusObservation?.invalidate() + statusObservation?.invalidate() + MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + MPRemoteCommandCenter.shared().playCommand.removeTarget(self) + MPRemoteCommandCenter.shared().pauseCommand.removeTarget(self) + MPRemoteCommandCenter.shared().nextTrackCommand.removeTarget(self) + MPRemoteCommandCenter.shared().previousTrackCommand.removeTarget(self) + } + + // MARK: - Private + + private func setupAudioSession() { + do { + let session = AVAudioSession.sharedInstance() + try session.setCategory(.playback, mode: .default) + try session.setActive(true) + } catch {} + } + + private func setupRemoteCommands() { + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.playCommand.isEnabled = true + commandCenter.pauseCommand.isEnabled = true + commandCenter.nextTrackCommand.isEnabled = true + commandCenter.previousTrackCommand.isEnabled = true + + commandCenter.playCommand.addTarget(self, action: #selector(handlePlayCommand)) + commandCenter.pauseCommand.addTarget(self, action: #selector(handlePauseCommand)) + commandCenter.nextTrackCommand.addTarget(self, action: #selector(handleNextCommand)) + commandCenter.previousTrackCommand.addTarget(self, action: #selector(handlePreviousCommand)) + + UIApplication.shared.beginReceivingRemoteControlEvents() + } + + @objc private func handlePlayCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { + play() + return .success + } + + @objc private func handlePauseCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { + pause() + return .success + } + + @objc private func handleNextCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { + nextSource() + return .success + } + + @objc private func handlePreviousCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { + previousSource() + return .success + } + + private func loadSource(at index: Int) { + currentIndex = index + let source = sources[index] + + guard let url = URL(string: source.url) else { return } + + statusObservation?.invalidate() + + let item = AVPlayerItem(url: url) + setupMetadataOutput(for: item) + player?.replaceCurrentItem(with: item) + + statusObservation = item.observe(\.status, options: [.new]) { [weak self] item, _ in + if item.status == .readyToPlay { + self?.updateNowPlayingInfo(title: source.title) + } + } + + updateNowPlayingInfo(title: source.title) + loadArtwork(for: source) + + DispatchQueue.main.async { + self.nowPlayingSink?.success(NowPlayingInfoMessage(title: nil)) + } + } + + private func setupMetadataOutput(for item: AVPlayerItem) { + let output = AVPlayerItemMetadataOutput(identifiers: nil) + output.setDelegate(self, queue: .main) + item.add(output) + metadataOutput = output + } + + private func observePlayerState() { + timeControlStatusObservation = player?.observe(\.timeControlStatus, options: [.new]) { [weak self] player, _ in + DispatchQueue.main.async { + let isPlaying = player.timeControlStatus == .playing + self?.playbackStateSink?.success(isPlaying) + + var info = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] + info[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0 + MPNowPlayingInfoCenter.default().nowPlayingInfo = info + } + } + } + + private func observeInterruptions() { + NotificationCenter.default.addObserver( + self, + selector: #selector(handleInterruption), + name: AVAudioSession.interruptionNotification, + object: nil + ) + } + + private func observeRouteChanges() { + NotificationCenter.default.addObserver( + self, + selector: #selector(handleRouteChange), + name: AVAudioSession.routeChangeNotification, + object: nil + ) + } + + @objc private func handleInterruption(_ notification: Notification) { + guard let userInfo = notification.userInfo, + let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, + let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return } + + switch type { + case .began: + pause() + case .ended: + if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt { + let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) + if options.contains(.shouldResume) { + play() + } + } + @unknown default: + break + } + } + + @objc private func handleRouteChange(_ notification: Notification) { + guard let userInfo = notification.userInfo, + let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt, + let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { return } + + if reason == .oldDeviceUnavailable { + pause() + } + } + + private func updateNowPlayingInfo(title: String?) { + var info: [String: Any] = [ + MPMediaItemPropertyTitle: title ?? getAppName() ?? "Radio", + MPMediaItemPropertyArtist: getAppName() ?? "", + MPNowPlayingInfoPropertyIsLiveStream: true, + ] + + if let existing = MPNowPlayingInfoCenter.default().nowPlayingInfo, + let artwork = existing[MPMediaItemPropertyArtwork] { + info[MPMediaItemPropertyArtwork] = artwork + } + + MPNowPlayingInfoCenter.default().nowPlayingInfo = info + } + + private func loadArtwork(for source: RadioSourceMessage) { + guard let artworkPath = source.artwork, !artworkPath.isEmpty else { return } + + if artworkPath.hasPrefix("http://") || artworkPath.hasPrefix("https://") { + loadArtworkFromURL(artworkPath) + } else { + loadArtworkFromAsset(artworkPath) + } + } + + private func loadArtworkFromURL(_ urlString: String) { + guard let url = URL(string: urlString) else { return } + URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in + guard let data = data, let image = UIImage(data: data) else { return } + DispatchQueue.main.async { + self?.setNowPlayingArtwork(image) + } + }.resume() + } + + private func loadArtworkFromAsset(_ assetName: String) { + guard let registrar = self.registrar else { return } + let assetKey = registrar.lookupKey(forAsset: assetName) + guard let assetPath = Bundle.main.path(forResource: assetKey, ofType: nil), + let image = UIImage(contentsOfFile: assetPath) else { return } + setNowPlayingArtwork(image) + } + + private func setNowPlayingArtwork(_ image: UIImage) { + let artwork = MPMediaItemArtwork(boundsSize: image.size) { _ in image } + var info = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] + info[MPMediaItemPropertyArtwork] = artwork + MPNowPlayingInfoCenter.default().nowPlayingInfo = info + } + + private func getAppName() -> String? { + return Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String + ?? Bundle.main.infoDictionary?["CFBundleName"] as? String + } +} + +// MARK: - AVPlayerItemMetadataOutputPushDelegate + +extension RadioPlayerService: AVPlayerItemMetadataOutputPushDelegate { + func metadataOutput(_ output: AVPlayerItemMetadataOutput, + didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], + from track: AVPlayerItemTrack?) { + for group in groups { + for item in group.items { + if let title = item.stringValue, !title.isEmpty { + updateNowPlayingInfo(title: title) + DispatchQueue.main.async { + self.nowPlayingSink?.success(NowPlayingInfoMessage(title: title)) + } + return + } + } + } + } +} diff --git a/packages/flutter_radio_player_ios/lib/flutter_radio_player_ios.dart b/packages/flutter_radio_player_ios/lib/flutter_radio_player_ios.dart new file mode 100644 index 0000000..533ce87 --- /dev/null +++ b/packages/flutter_radio_player_ios/lib/flutter_radio_player_ios.dart @@ -0,0 +1,71 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_radio_player_platform_interface/flutter_radio_player_platform_interface.dart'; + +import 'src/messages.g.dart'; + +class FlutterRadioPlayerIos extends FlutterRadioPlayerPlatform { + final RadioPlayerHostApi _hostApi = RadioPlayerHostApi(); + + static void registerWith() { + FlutterRadioPlayerPlatform.instance = FlutterRadioPlayerIos(); + } + + @override + Future initialize( + List sources, { + bool playWhenReady = false, + }) { + return _hostApi.initialize( + sources.map(_toMessage).toList(), + playWhenReady, + ); + } + + @override + Future play() => _hostApi.play(); + + @override + Future pause() => _hostApi.pause(); + + @override + Future playOrPause() => _hostApi.playOrPause(); + + @override + Future setVolume(double volume) => _hostApi.setVolume(volume); + + @override + Future getVolume() => _hostApi.getVolume(); + + @override + Future nextSource() => _hostApi.nextSource(); + + @override + Future previousSource() => _hostApi.previousSource(); + + @override + Future jumpToSourceAtIndex(int index) => + _hostApi.jumpToSourceAtIndex(index); + + @override + Future dispose() => _hostApi.dispose(); + + @override + @visibleForTesting + Stream get isPlayingStream => onPlaybackStateChanged(); + + @override + Stream get nowPlayingStream => + onNowPlayingChanged().map((msg) => NowPlayingInfo(title: msg.title)); + + @override + Stream get volumeStream => onVolumeChanged() + .map((msg) => VolumeInfo(volume: msg.volume, isMuted: msg.isMuted)); + + static RadioSourceMessage _toMessage(RadioSource source) { + return RadioSourceMessage( + url: source.url, + title: source.title, + artwork: source.artwork, + ); + } +} diff --git a/packages/flutter_radio_player_ios/lib/src/messages.g.dart b/packages/flutter_radio_player_ios/lib/src/messages.g.dart new file mode 100644 index 0000000..ca74975 --- /dev/null +++ b/packages/flutter_radio_player_ios/lib/src/messages.g.dart @@ -0,0 +1,480 @@ +// Autogenerated from Pigeon (v26.1.7), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, omit_obvious_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} +bool _deepEquals(Object? a, Object? b) { + if (a is List && b is List) { + return a.length == b.length && + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + } + if (a is Map && b is Map) { + return a.length == b.length && a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); + } + return a == b; +} + + +class RadioSourceMessage { + RadioSourceMessage({ + required this.url, + this.title, + this.artwork, + }); + + String url; + + String? title; + + String? artwork; + + List _toList() { + return [ + url, + title, + artwork, + ]; + } + + Object encode() { + return _toList(); } + + static RadioSourceMessage decode(Object result) { + result as List; + return RadioSourceMessage( + url: result[0]! as String, + title: result[1] as String?, + artwork: result[2] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! RadioSourceMessage || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + +class NowPlayingInfoMessage { + NowPlayingInfoMessage({ + this.title, + }); + + String? title; + + List _toList() { + return [ + title, + ]; + } + + Object encode() { + return _toList(); } + + static NowPlayingInfoMessage decode(Object result) { + result as List; + return NowPlayingInfoMessage( + title: result[0] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! NowPlayingInfoMessage || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + +class VolumeInfoMessage { + VolumeInfoMessage({ + required this.volume, + required this.isMuted, + }); + + double volume; + + bool isMuted; + + List _toList() { + return [ + volume, + isMuted, + ]; + } + + Object encode() { + return _toList(); } + + static VolumeInfoMessage decode(Object result) { + result as List; + return VolumeInfoMessage( + volume: result[0]! as double, + isMuted: result[1]! as bool, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! VolumeInfoMessage || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is RadioSourceMessage) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is NowPlayingInfoMessage) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is VolumeInfoMessage) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + return RadioSourceMessage.decode(readValue(buffer)!); + case 130: + return NowPlayingInfoMessage.decode(readValue(buffer)!); + case 131: + return VolumeInfoMessage.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +const StandardMethodCodec pigeonMethodCodec = StandardMethodCodec(_PigeonCodec()); + +class RadioPlayerHostApi { + /// Constructor for [RadioPlayerHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + RadioPlayerHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future initialize(List sources, bool playWhenReady) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerHostApi.initialize$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([sources, playWhenReady]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future play() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerHostApi.play$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future pause() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerHostApi.pause$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future playOrPause() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerHostApi.playOrPause$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future setVolume(double volume) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerHostApi.setVolume$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([volume]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future getVolume() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerHostApi.getVolume$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as double?)!; + } + } + + Future nextSource() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerHostApi.nextSource$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future previousSource() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerHostApi.previousSource$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future jumpToSourceAtIndex(int index) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerHostApi.jumpToSourceAtIndex$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([index]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future dispose() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerHostApi.dispose$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } +} + +Stream onPlaybackStateChanged( {String instanceName = ''}) { + if (instanceName.isNotEmpty) { + instanceName = '.$instanceName'; + } + final EventChannel onPlaybackStateChangedChannel = + EventChannel('dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerEventApi.onPlaybackStateChanged$instanceName', pigeonMethodCodec); + return onPlaybackStateChangedChannel.receiveBroadcastStream().map((dynamic event) { + return event as bool; + }); +} + +Stream onNowPlayingChanged( {String instanceName = ''}) { + if (instanceName.isNotEmpty) { + instanceName = '.$instanceName'; + } + final EventChannel onNowPlayingChangedChannel = + EventChannel('dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerEventApi.onNowPlayingChanged$instanceName', pigeonMethodCodec); + return onNowPlayingChangedChannel.receiveBroadcastStream().map((dynamic event) { + return event as NowPlayingInfoMessage; + }); +} + +Stream onVolumeChanged( {String instanceName = ''}) { + if (instanceName.isNotEmpty) { + instanceName = '.$instanceName'; + } + final EventChannel onVolumeChangedChannel = + EventChannel('dev.flutter.pigeon.flutter_radio_player_ios.RadioPlayerEventApi.onVolumeChanged$instanceName', pigeonMethodCodec); + return onVolumeChangedChannel.receiveBroadcastStream().map((dynamic event) { + return event as VolumeInfoMessage; + }); +} + diff --git a/packages/flutter_radio_player_ios/pigeons/messages.dart b/packages/flutter_radio_player_ios/pigeons/messages.dart new file mode 100644 index 0000000..fa7093c --- /dev/null +++ b/packages/flutter_radio_player_ios/pigeons/messages.dart @@ -0,0 +1,42 @@ +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + swiftOut: + 'ios/flutter_radio_player_ios/Sources/flutter_radio_player_ios/Messages.g.swift', +)) +class RadioSourceMessage { + late String url; + late String? title; + late String? artwork; +} + +class NowPlayingInfoMessage { + late String? title; +} + +class VolumeInfoMessage { + late double volume; + late bool isMuted; +} + +@HostApi() +abstract class RadioPlayerHostApi { + void initialize(List sources, bool playWhenReady); + void play(); + void pause(); + void playOrPause(); + void setVolume(double volume); + double getVolume(); + void nextSource(); + void previousSource(); + void jumpToSourceAtIndex(int index); + void dispose(); +} + +@EventChannelApi() +abstract class RadioPlayerEventApi { + bool onPlaybackStateChanged(); + NowPlayingInfoMessage onNowPlayingChanged(); + VolumeInfoMessage onVolumeChanged(); +} diff --git a/packages/flutter_radio_player_ios/pubspec.lock b/packages/flutter_radio_player_ios/pubspec.lock new file mode 100644 index 0000000..6b9a915 --- /dev/null +++ b/packages/flutter_radio_player_ios/pubspec.lock @@ -0,0 +1,372 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "5b7468c326d2f8a4f630056404ca0d291ade42918f4a3c6233618e724f39da8e" + url: "https://pub.dev" + source: hosted + version: "92.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "70e4b1ef8003c64793a9e268a551a82869a8a96f39deb73dea28084b0e8bf75e" + url: "https://pub.dev" + source: hosted + version: "9.0.0" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "7931c90b84bc573fef103548e354258ae4c9d28d140e41961df6843c5d60d4d8" + url: "https://pub.dev" + source: hosted + version: "8.12.3" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + url: "https://pub.dev" + source: hosted + version: "4.11.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b + url: "https://pub.dev" + source: hosted + version: "3.1.3" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + flutter_radio_player_platform_interface: + dependency: "direct main" + description: + path: "../flutter_radio_player_platform_interface" + relative: true + source: path + version: "4.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pigeon: + dependency: "direct dev" + description: + name: pigeon + sha256: b12f739047654cd65338dc7e9c6c03cbd4a1de179364a45c9e8bbeccf23729c6 + url: "https://pub.dev" + source: hosted + version: "26.1.7" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/packages/flutter_radio_player_ios/pubspec.yaml b/packages/flutter_radio_player_ios/pubspec.yaml new file mode 100644 index 0000000..5045781 --- /dev/null +++ b/packages/flutter_radio_player_ios/pubspec.yaml @@ -0,0 +1,28 @@ +name: flutter_radio_player_ios +description: iOS implementation of flutter_radio_player. +version: 4.0.0 +homepage: https://github.com/Sithira/FlutterRadioPlayer + +environment: + sdk: '>=3.4.3 <4.0.0' + flutter: '>=3.3.0' + +flutter: + plugin: + implements: flutter_radio_player + platforms: + ios: + dartPluginClass: FlutterRadioPlayerIos + pluginClass: FlutterRadioPlayerPlugin + +dependencies: + flutter: + sdk: flutter + flutter_radio_player_platform_interface: + path: ../flutter_radio_player_platform_interface + +dev_dependencies: + pigeon: ^26.0.0 + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 diff --git a/packages/flutter_radio_player_ios/test/flutter_radio_player_ios_test.dart b/packages/flutter_radio_player_ios/test/flutter_radio_player_ios_test.dart new file mode 100644 index 0000000..5c2ff83 --- /dev/null +++ b/packages/flutter_radio_player_ios/test/flutter_radio_player_ios_test.dart @@ -0,0 +1,13 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_radio_player_ios/flutter_radio_player_ios.dart'; +import 'package:flutter_radio_player_platform_interface/flutter_radio_player_platform_interface.dart'; + +void main() { + test('registerWith sets platform instance', () { + FlutterRadioPlayerIos.registerWith(); + expect( + FlutterRadioPlayerPlatform.instance, + isA(), + ); + }); +} diff --git a/packages/flutter_radio_player_platform_interface/analysis_options.yaml b/packages/flutter_radio_player_platform_interface/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/packages/flutter_radio_player_platform_interface/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/packages/flutter_radio_player_platform_interface/lib/flutter_radio_player_platform_interface.dart b/packages/flutter_radio_player_platform_interface/lib/flutter_radio_player_platform_interface.dart new file mode 100644 index 0000000..1b4d7df --- /dev/null +++ b/packages/flutter_radio_player_platform_interface/lib/flutter_radio_player_platform_interface.dart @@ -0,0 +1,83 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'src/types/types.dart'; + +export 'src/types/types.dart'; + +abstract class FlutterRadioPlayerPlatform extends PlatformInterface { + FlutterRadioPlayerPlatform() : super(token: _token); + + static final Object _token = Object(); + + static FlutterRadioPlayerPlatform? _instance; + + static FlutterRadioPlayerPlatform get instance { + if (_instance == null) { + throw StateError( + 'FlutterRadioPlayerPlatform has not been set. ' + 'Ensure a platform implementation is registered.', + ); + } + return _instance!; + } + + static set instance(FlutterRadioPlayerPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future initialize( + List sources, { + bool playWhenReady = false, + }) { + throw UnimplementedError('initialize() has not been implemented.'); + } + + Future play() { + throw UnimplementedError('play() has not been implemented.'); + } + + Future pause() { + throw UnimplementedError('pause() has not been implemented.'); + } + + Future playOrPause() { + throw UnimplementedError('playOrPause() has not been implemented.'); + } + + Future setVolume(double volume) { + throw UnimplementedError('setVolume() has not been implemented.'); + } + + Future getVolume() { + throw UnimplementedError('getVolume() has not been implemented.'); + } + + Future nextSource() { + throw UnimplementedError('nextSource() has not been implemented.'); + } + + Future previousSource() { + throw UnimplementedError('previousSource() has not been implemented.'); + } + + Future jumpToSourceAtIndex(int index) { + throw UnimplementedError('jumpToSourceAtIndex() has not been implemented.'); + } + + Future dispose() { + throw UnimplementedError('dispose() has not been implemented.'); + } + + Stream get isPlayingStream { + throw UnimplementedError('isPlayingStream has not been implemented.'); + } + + Stream get nowPlayingStream { + throw UnimplementedError('nowPlayingStream has not been implemented.'); + } + + Stream get volumeStream { + throw UnimplementedError('volumeStream has not been implemented.'); + } +} diff --git a/packages/flutter_radio_player_platform_interface/lib/src/types/now_playing_info.dart b/packages/flutter_radio_player_platform_interface/lib/src/types/now_playing_info.dart new file mode 100644 index 0000000..2cc14c9 --- /dev/null +++ b/packages/flutter_radio_player_platform_interface/lib/src/types/now_playing_info.dart @@ -0,0 +1,18 @@ +class NowPlayingInfo { + const NowPlayingInfo({this.title}); + + final String? title; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is NowPlayingInfo && + runtimeType == other.runtimeType && + title == other.title; + + @override + int get hashCode => title.hashCode; + + @override + String toString() => 'NowPlayingInfo(title: $title)'; +} diff --git a/packages/flutter_radio_player_platform_interface/lib/src/types/radio_source.dart b/packages/flutter_radio_player_platform_interface/lib/src/types/radio_source.dart new file mode 100644 index 0000000..aa692c0 --- /dev/null +++ b/packages/flutter_radio_player_platform_interface/lib/src/types/radio_source.dart @@ -0,0 +1,23 @@ +class RadioSource { + const RadioSource({required this.url, this.title, this.artwork}); + + final String url; + final String? title; + final String? artwork; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is RadioSource && + runtimeType == other.runtimeType && + url == other.url && + title == other.title && + artwork == other.artwork; + + @override + int get hashCode => Object.hash(url, title, artwork); + + @override + String toString() => + 'RadioSource(url: $url, title: $title, artwork: $artwork)'; +} diff --git a/packages/flutter_radio_player_platform_interface/lib/src/types/types.dart b/packages/flutter_radio_player_platform_interface/lib/src/types/types.dart new file mode 100644 index 0000000..900433e --- /dev/null +++ b/packages/flutter_radio_player_platform_interface/lib/src/types/types.dart @@ -0,0 +1,3 @@ +export 'now_playing_info.dart'; +export 'radio_source.dart'; +export 'volume_info.dart'; diff --git a/packages/flutter_radio_player_platform_interface/lib/src/types/volume_info.dart b/packages/flutter_radio_player_platform_interface/lib/src/types/volume_info.dart new file mode 100644 index 0000000..b8ab6c0 --- /dev/null +++ b/packages/flutter_radio_player_platform_interface/lib/src/types/volume_info.dart @@ -0,0 +1,20 @@ +class VolumeInfo { + const VolumeInfo({required this.volume, required this.isMuted}); + + final double volume; + final bool isMuted; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is VolumeInfo && + runtimeType == other.runtimeType && + volume == other.volume && + isMuted == other.isMuted; + + @override + int get hashCode => Object.hash(volume, isMuted); + + @override + String toString() => 'VolumeInfo(volume: $volume, isMuted: $isMuted)'; +} diff --git a/pubspec.lock b/packages/flutter_radio_player_platform_interface/pubspec.lock similarity index 66% rename from pubspec.lock rename to packages/flutter_radio_player_platform_interface/pubspec.lock index 72000bb..f42948e 100644 --- a/pubspec.lock +++ b/packages/flutter_radio_player_platform_interface/pubspec.lock @@ -5,50 +5,50 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" flutter: dependency: "direct main" description: flutter @@ -71,26 +71,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -103,10 +103,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -119,18 +119,18 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.17.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" plugin_platform_interface: dependency: "direct main" description: @@ -143,71 +143,71 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.2" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.7" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "15.0.2" sdks: - dart: ">=3.4.3 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/packages/flutter_radio_player_platform_interface/pubspec.yaml b/packages/flutter_radio_player_platform_interface/pubspec.yaml new file mode 100644 index 0000000..950ad5b --- /dev/null +++ b/packages/flutter_radio_player_platform_interface/pubspec.yaml @@ -0,0 +1,18 @@ +name: flutter_radio_player_platform_interface +description: A common platform interface for the flutter_radio_player plugin. +version: 4.0.0 +homepage: https://github.com/Sithira/FlutterRadioPlayer + +environment: + sdk: '>=3.4.3 <4.0.0' + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.1.7 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 diff --git a/packages/flutter_radio_player_platform_interface/test/flutter_radio_player_platform_interface_test.dart b/packages/flutter_radio_player_platform_interface/test/flutter_radio_player_platform_interface_test.dart new file mode 100644 index 0000000..b08ed24 --- /dev/null +++ b/packages/flutter_radio_player_platform_interface/test/flutter_radio_player_platform_interface_test.dart @@ -0,0 +1,66 @@ +import 'package:flutter_radio_player_platform_interface/flutter_radio_player_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class _FakePlatform extends FlutterRadioPlayerPlatform with MockPlatformInterfaceMixin {} + +void main() { + group('FlutterRadioPlayerPlatform', () { + test('can be set with a valid token', () { + final fake = _FakePlatform(); + FlutterRadioPlayerPlatform.instance = fake; + expect(FlutterRadioPlayerPlatform.instance, fake); + }); + + test('all methods throw UnimplementedError by default', () { + final fake = _FakePlatform(); + FlutterRadioPlayerPlatform.instance = fake; + final platform = FlutterRadioPlayerPlatform.instance; + + expect(() => platform.initialize([]), throwsUnimplementedError); + expect(() => platform.play(), throwsUnimplementedError); + expect(() => platform.pause(), throwsUnimplementedError); + expect(() => platform.playOrPause(), throwsUnimplementedError); + expect(() => platform.setVolume(0.5), throwsUnimplementedError); + expect(() => platform.getVolume(), throwsUnimplementedError); + expect(() => platform.nextSource(), throwsUnimplementedError); + expect(() => platform.previousSource(), throwsUnimplementedError); + expect(() => platform.jumpToSourceAtIndex(0), throwsUnimplementedError); + expect(() => platform.dispose(), throwsUnimplementedError); + expect(() => platform.isPlayingStream, throwsUnimplementedError); + expect(() => platform.nowPlayingStream, throwsUnimplementedError); + expect(() => platform.volumeStream, throwsUnimplementedError); + }); + }); + + group('RadioSource', () { + test('equality', () { + const a = RadioSource(url: 'http://example.com', title: 'Test'); + const b = RadioSource(url: 'http://example.com', title: 'Test'); + const c = RadioSource(url: 'http://other.com'); + expect(a, equals(b)); + expect(a, isNot(equals(c))); + expect(a.hashCode, equals(b.hashCode)); + }); + }); + + group('NowPlayingInfo', () { + test('equality', () { + const a = NowPlayingInfo(title: 'Song'); + const b = NowPlayingInfo(title: 'Song'); + const c = NowPlayingInfo(); + expect(a, equals(b)); + expect(a, isNot(equals(c))); + }); + }); + + group('VolumeInfo', () { + test('equality', () { + const a = VolumeInfo(volume: 0.5, isMuted: false); + const b = VolumeInfo(volume: 0.5, isMuted: false); + const c = VolumeInfo(volume: 1.0, isMuted: true); + expect(a, equals(b)); + expect(a, isNot(equals(c))); + }); + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index 2bce823..fdb91d8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,72 +1,6 @@ -name: flutter_radio_player -description: "Online Radio Player for Flutter which enable to play streaming URL. Supports Android and iOS as well as WearOs and watchOs" -version: 3.0.2 -homepage: https://github.com/Sithira/FlutterRadioPlayer +name: flutter_radio_player_workspace +description: Monorepo workspace for flutter_radio_player packages. +publish_to: 'none' environment: sdk: '>=3.4.3 <4.0.0' - flutter: '>=3.3.0' - -dependencies: - flutter: - sdk: flutter - plugin_platform_interface: ^2.0.2 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^3.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. -flutter: - # This section identifies this Flutter project as a plugin project. - # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) - # which should be registered in the plugin registry. This is required for - # using method channels. - # The Android 'package' specifies package in which the registered class is. - # This is required for using method channels on Android. - # The 'ffiPlugin' specifies that native code should be built and bundled. - # This is required for using `dart:ffi`. - # All these are used by the tooling to maintain consistency when - # adding or updating assets for this project. - plugin: - platforms: - android: - package: me.sithiramunasinghe.flutter.flutter_radio_player - pluginClass: FlutterRadioPlayerPlugin - ios: - pluginClass: FlutterRadioPlayerPlugin - - # To add assets to your plugin package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # To add custom fonts to your plugin package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages diff --git a/test/flutter_radio_player_method_channel_test.dart b/test/flutter_radio_player_method_channel_test.dart deleted file mode 100644 index f4977ab..0000000 --- a/test/flutter_radio_player_method_channel_test.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_radio_player/flutter_radio_player_method_channel.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - MethodChannelFlutterRadioPlayer platform = MethodChannelFlutterRadioPlayer(); - const MethodChannel channel = MethodChannel('flutter_radio_player'); - - setUp(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - channel, - (MethodCall methodCall) async { - return '42'; - }, - ); - }); - - tearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, null); - }); - - test('getPlatformVersion', () async { - expect('42', '42'); - }); -} diff --git a/test/flutter_radio_player_test.dart b/test/flutter_radio_player_test.dart deleted file mode 100644 index 4dc4aaa..0000000 --- a/test/flutter_radio_player_test.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'package:flutter_radio_player/data/flutter_radio_player_event.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_radio_player/flutter_radio_player.dart'; -import 'package:flutter_radio_player/flutter_radio_player_platform_interface.dart'; -import 'package:flutter_radio_player/flutter_radio_player_method_channel.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -class MockFlutterRadioPlayerPlatform - with MockPlatformInterfaceMixin - implements FlutterRadioPlayerPlatform { - @override - Future getPlatformVersion() => Future.value('42'); - - @override - Future play() { - // TODO: implement play - throw UnimplementedError(); - } - - @override - Future changeVolume(double volume) { - // TODO: implement changeVolume - throw UnimplementedError(); - } - - @override - Stream getDeviceVolumeChangedStream() { - // TODO: implement getDeviceVolumeChangedStream - throw UnimplementedError(); - } - - @override - Stream getIsPlayingStream() { - // TODO: implement getIsPlayingStream - throw UnimplementedError(); - } - - @override - Stream getNowPlayingStream() { - // TODO: implement getNowPlayingStream - throw UnimplementedError(); - } - - @override - Future getVolume() { - // TODO: implement getVolume - throw UnimplementedError(); - } - - @override - Future initialize( - List> sources, bool playWhenReady) { - // TODO: implement initialize - throw UnimplementedError(); - } - - @override - Future jumpToSourceIndex(int index) { - // TODO: implement jumpToSourceIndex - throw UnimplementedError(); - } - - @override - Future nextSource() { - // TODO: implement nextSource - throw UnimplementedError(); - } - - @override - Future pause() { - // TODO: implement pause - throw UnimplementedError(); - } - - @override - Future playOrPause() { - // TODO: implement playOrPause - throw UnimplementedError(); - } - - @override - Future previousSource() { - // TODO: implement previousSource - throw UnimplementedError(); - } -} - -void main() { - final FlutterRadioPlayerPlatform initialPlatform = - FlutterRadioPlayerPlatform.instance; - - test('$MethodChannelFlutterRadioPlayer is the default instance', () { - expect(initialPlatform, isInstanceOf()); - }); - - test('getPlatformVersion', () async { - FlutterRadioPlayer flutterRadioPlayerPlugin = FlutterRadioPlayer(); - MockFlutterRadioPlayerPlatform fakePlatform = - MockFlutterRadioPlayerPlatform(); - FlutterRadioPlayerPlatform.instance = fakePlatform; - - expect('42', '42'); - }); -} diff --git a/xcode_required_capabilities.png b/xcode_required_capabilities.png new file mode 100644 index 0000000..fa6c87a Binary files /dev/null and b/xcode_required_capabilities.png differ