diff --git a/.github/workflows/build-example.yml b/.github/workflows/build-example.yml deleted file mode 100644 index 53ea21f99..000000000 --- a/.github/workflows/build-example.yml +++ /dev/null @@ -1,213 +0,0 @@ -name: build example -on: - workflow_dispatch: - inputs: - flutter_version: - description: 'Flutter Version' - required: false - default: 'any' - type: choice - options: - - 'any' - - '3.35.x' - - '3.32.x' - - '3.29.x' - - '3.27.x' - flutter_channel: - description: 'Flutter Channel' - required: false - default: 'stable' - type: choice - options: - - 'stable' - - 'beta' - - 'dev' - - 'master' - enable_android: - description: 'Build Android' - required: false - default: true - type: boolean - enable_web: - description: 'Build Web' - required: false - default: true - type: boolean - enable_ios: - description: 'Build IOS' - required: false - default: true - type: boolean - enable_windows: - description: 'Build Windows' - required: false - default: true - type: boolean - enable_linux: - description: 'Build Linux' - required: false - default: true - type: boolean - enable_macos: - description: 'Build MacOS' - required: false - default: true - type: boolean - upload_pages_artifact: - description: 'Upload build artifact for GH pages' - required: false - default: false - type: boolean - workflow_call: - inputs: - flutter_version: - required: false - default: '3.35.7' - type: string - flutter_channel: - required: false - default: 'stable' - type: string - enable_android: - required: false - default: true - type: boolean - enable_web: - required: false - default: true - type: boolean - enable_ios: - required: false - default: true - type: boolean - enable_windows: - required: false - default: true - type: boolean - enable_linux: - required: false - default: true - type: boolean - enable_macos: - required: false - default: true - type: boolean - upload_pages_artifact: - required: false - default: false - type: boolean - -jobs: - web: - runs-on: ubuntu-latest - timeout-minutes: 30 - if: inputs.enable_web - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ inputs.flutter_version }} - channel: ${{ inputs.flutter_channel }} - - uses: bluefireteam/melos-action@v3 - - - name: Example app - Build Web app - working-directory: ./packages/audioplayers/example - run: flutter build web --base-href "/audioplayers/" - - name: Example app - Build Web app in WASM - working-directory: ./packages/audioplayers/example - run: flutter build web --base-href "/audioplayers/" --wasm - - name: Upload pages artifact - if: inputs.upload_pages_artifact - uses: actions/upload-pages-artifact@v3 - with: - path: ./packages/audioplayers/example/build/web - - android: - runs-on: ubuntu-latest - timeout-minutes: 60 - if: inputs.enable_android - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ inputs.flutter_version }} - channel: ${{ inputs.flutter_channel }} - - uses: bluefireteam/melos-action@v3 - - - name: Example App - Build Android APK - working-directory: ./packages/audioplayers/example - run: flutter build apk --release - - ios: - runs-on: macos-14 - timeout-minutes: 30 - if: inputs.enable_ios - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ inputs.flutter_version }} - channel: ${{ inputs.flutter_channel }} - - uses: bluefireteam/melos-action@v3 - - - name: Example app - Build iOS - working-directory: ./packages/audioplayers/example - run: flutter build ios --release --no-codesign - - macos: - runs-on: macos-14 - timeout-minutes: 30 - if: inputs.enable_macos - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ inputs.flutter_version }} - channel: ${{ inputs.flutter_channel }} - - uses: bluefireteam/melos-action@v3 - - - name: Example app - Build macOS - working-directory: ./packages/audioplayers/example - run: flutter build macos --release - - windows: - runs-on: windows-latest - timeout-minutes: 30 - if: inputs.enable_windows - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ inputs.flutter_version }} - channel: ${{ inputs.flutter_channel }} - - uses: bluefireteam/melos-action@v3 - - - name: Example app - Build Windows app - working-directory: ./packages/audioplayers/example - run: flutter build windows --release - - linux: - runs-on: ubuntu-latest - timeout-minutes: 30 - if: inputs.enable_linux - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ inputs.flutter_version }} - channel: ${{ inputs.flutter_channel }} - - uses: bluefireteam/melos-action@v3 - - - name: Install Flutter requirements for Linux - run: | - sudo apt-get update - sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev - - name: Install GStreamer - # Install libunwind-dev, see https://github.com/actions/runner-images/issues/6399#issuecomment-1285011525 - run: | - sudo apt install -y libunwind-dev - sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev - - - name: Example app - Build Linux app - working-directory: ./packages/audioplayers/example - run: flutter build linux --release diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 535e9d10e..000000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: build -on: - push: - branches: - - main - workflow_dispatch: - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - call-build-example: - uses: ./.github/workflows/build-example.yml - with: - upload_pages_artifact: true - - call-test: - needs: call-build-example - uses: ./.github/workflows/test.yml - with: - enable_min_version: true - - deploy-github-pages: - needs: - - call-build-example - - call-test - permissions: - pages: write - id-token: write - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/build_check.yml b/.github/workflows/build_check.yml new file mode 100644 index 000000000..290beb33f --- /dev/null +++ b/.github/workflows/build_check.yml @@ -0,0 +1,118 @@ +name: Build Check + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +jobs: + analyze: + name: Analyze & Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: 'stable' + + - name: Get Dependencies + run: flutter pub get + + - name: Check Formatting + run: dart format --output=none --set-exit-if-changed . + + - name: Analyze + run: flutter analyze . + + android-build: + name: Android Check + needs: analyze + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: 'corretto' + java-version: '17' + + - uses: subosito/flutter-action@v2 + with: + channel: 'stable' + + # Test Unitaire (Logique Dart) + - name: Get Dependencies (Package) + working-directory: packages/audioplayers + run: flutter pub get + + - name: Run Unit Tests + working-directory: packages/audioplayers + run: flutter test + + # Build Android (Vérification de la compilation) + - name: Get Dependencies (Example) + working-directory: packages/audioplayers/example + run: flutter pub get + + - name: Build APK (Release) + working-directory: packages/audioplayers/example + run: flutter build apk --release --no-pub + + linux-build: + name: Linux Check + needs: analyze + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y ninja-build libgtk-3-dev xvfb libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev + + - uses: subosito/flutter-action@v2 + with: + channel: 'stable' + + - name: Enable Linux Support + run: flutter config --enable-linux-desktop + + - name: Get Dependencies + working-directory: packages/audioplayers/example + run: flutter pub get + + - name: Run Integration Tests + working-directory: packages/audioplayers/example + run: xvfb-run flutter test integration_test/app_test.dart -d linux + + - name: Build Linux (Release) + working-directory: packages/audioplayers/example + run: flutter build linux --release --no-pub + + windows-build: + name: Windows Check + needs: analyze + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - uses: subosito/flutter-action@v2 + with: + channel: 'stable' + + - name: Enable Windows Support + run: flutter config --enable-windows-desktop + + - name: Get Dependencies + working-directory: packages/audioplayers/example + run: flutter pub get + + - name: Run Integration Tests + working-directory: packages/audioplayers/example + run: flutter test integration_test/app_test.dart -d windows + + - name: Build Windows (Release) + working-directory: packages/audioplayers/example + run: flutter build windows --release --no-pub \ No newline at end of file diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml deleted file mode 100644 index fa4286671..000000000 --- a/.github/workflows/pull-request.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: pull-request -on: - pull_request: - types: - - opened - - reopened - - synchronize - - ready_for_review - -jobs: - call-min-flutter-test: - uses: ./.github/workflows/test.yml - with: - flutter_version: '3.27.4' - fatal_warnings: false - enable_android: ${{ github.event.pull_request.draft == false }} - enable_web: ${{ github.event.pull_request.draft == false }} - enable_ios: ${{ github.event.pull_request.draft == false }} - enable_windows: ${{ github.event.pull_request.draft == false }} - enable_linux: ${{ github.event.pull_request.draft == false }} - enable_macos: ${{ github.event.pull_request.draft == false }} - call-test: - uses: ./.github/workflows/test.yml - with: - enable_android: ${{ github.event.pull_request.draft == false }} - enable_web: ${{ github.event.pull_request.draft == false }} - enable_ios: ${{ github.event.pull_request.draft == false }} - enable_windows: ${{ github.event.pull_request.draft == false }} - enable_linux: ${{ github.event.pull_request.draft == false }} - enable_macos: ${{ github.event.pull_request.draft == false }} - enable_min_version: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 5bdda5693..000000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,471 +0,0 @@ -name: test -on: - workflow_dispatch: - inputs: - flutter_version: - description: 'Flutter Version' - required: false - default: 'any' - type: choice - options: - - 'any' - - '3.35.x' - - '3.32.x' - - '3.29.x' - - '3.27.x' - flutter_channel: - description: 'Flutter Channel' - required: false - default: 'stable' - type: choice - options: - - 'stable' - - 'beta' - - 'dev' - - 'master' - fatal_warnings: - description: 'Treat warnings as fatal' - required: false - default: true - type: boolean - enable_android: - description: 'Test Android' - required: false - default: true - type: boolean - enable_web: - description: 'Test Web' - required: false - default: true - type: boolean - enable_ios: - description: 'Test IOS' - required: false - default: true - type: boolean - enable_windows: - description: 'Test Windows' - required: false - default: true - type: boolean - enable_linux: - description: 'Test Linux' - required: false - default: true - type: boolean - enable_macos: - description: 'Test MacOS' - required: false - default: true - type: boolean - enable_min_version: - description: 'Test Platform min version' - required: false - default: false - type: boolean - - workflow_call: - inputs: - flutter_version: - required: false - default: '3.35.7' - type: string - flutter_channel: - required: false - default: 'stable' - type: string - fatal_warnings: - required: false - default: true - type: boolean - enable_android: - required: false - default: true - type: boolean - enable_web: - required: false - default: true - type: boolean - enable_ios: - required: false - default: true - type: boolean - enable_windows: - required: false - default: true - type: boolean - enable_linux: - required: false - default: true - type: boolean - enable_macos: - required: false - default: true - type: boolean - enable_min_version: - required: false - default: false - type: boolean - -jobs: - test: - runs-on: ubuntu-latest - timeout-minutes: 30 - - steps: - - uses: actions/checkout@v4 - with: - # Full git history needed for `super-linter` - fetch-depth: 0 - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ inputs.flutter_version }} - channel: ${{ inputs.flutter_channel }} - - uses: bluefireteam/melos-action@v3 - - - run: melos format --set-exit-if-changed - - run: flutter analyze ${{ inputs.fatal_warnings && '--fatal-infos' || '--no-fatal-warnings' }} - - run: melos run test - - - name: Lint Code Base - uses: super-linter/super-linter/slim@v5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VALIDATE_ALL_CODEBASE: false - DEFAULT_BRANCH: main - VALIDATE_KOTLIN_ANDROID: true - VALIDATE_CLANG_FORMAT: true - - name: Lint Swift - # TODO: check if swift-format can be integrated in super-linter, as soon as Alpine is supported - # https://github.com/apple/swift-docker/issues/231 - # https://github.com/super-linter/super-linter/pull/4568 - run: | - docker run --rm --workdir=/work --volume=$PWD:/work mtgto/swift-format:5.8 \ - lint --parallel --strict --recursive packages/audioplayers_darwin - - web: - runs-on: ubuntu-latest - timeout-minutes: 30 - if: inputs.enable_web - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ inputs.flutter_version }} - channel: ${{ inputs.flutter_channel }} - - uses: bluefireteam/melos-action@v3 - - uses: nanasess/setup-chromedriver@v2 - - - name: Run Flutter integration tests - working-directory: ./packages/audioplayers/example - run: | - export DISPLAY=:99 - sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & - - chromedriver --port=4444 & - - ( cd server; dart run bin/server.dart ) & - flutter drive \ - --driver=test_driver/integration_test.dart \ - --target=integration_test/platform_test.dart \ - -d web-server \ - --web-browser-flag="--autoplay-policy=no-user-gesture-required" \ - --web-browser-flag="--disable-web-security" \ - --dart-define USE_LOCAL_SERVER=true - - flutter drive \ - --driver=test_driver/integration_test.dart \ - --target=integration_test/lib_test.dart \ - -d web-server \ - --web-browser-flag="--autoplay-policy=no-user-gesture-required" \ - --web-browser-flag="--disable-web-security" \ - --dart-define USE_LOCAL_SERVER=true - - flutter drive \ - --driver=test_driver/integration_test.dart \ - --target=integration_test/app_test.dart \ - -d web-server \ - --web-browser-flag="--autoplay-policy=no-user-gesture-required" \ - --web-browser-flag="--disable-web-security" \ - --dart-define USE_LOCAL_SERVER=true - - android-min: - runs-on: ubuntu-latest - timeout-minutes: 90 - if: inputs.enable_min_version && inputs.enable_android - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ inputs.flutter_version }} - channel: ${{ inputs.flutter_channel }} - - uses: bluefireteam/melos-action@v3 - - name: Setup Android Emulator - timeout-minutes: 10 - run: bash ./scripts/ci/setup-android.sh 24 default - - name: Run Flutter integration tests - working-directory: ./packages/audioplayers/example - # Need to execute lib and app tests one by one, see: https://github.com/flutter/flutter/issues/101031 - run: | - ( cd server; dart run bin/server.dart ) & - flutter test integration_test/platform_test.dart --dart-define USE_LOCAL_SERVER=true --dart-define TEST_FEATURE_LOW_LATENCY=false --dart-define TEST_FEATURE_BYTES_SOURCE=false --dart-define TEST_FEATURE_PLAYBACK_RATE=false - flutter test integration_test/lib_test.dart --dart-define USE_LOCAL_SERVER=true --dart-define TEST_FEATURE_LOW_LATENCY=false --dart-define TEST_FEATURE_BYTES_SOURCE=false --dart-define TEST_FEATURE_PLAYBACK_RATE=false - flutter test integration_test/app_test.dart --dart-define USE_LOCAL_SERVER=true --dart-define TEST_FEATURE_LOW_LATENCY=false --dart-define TEST_FEATURE_BYTES_SOURCE=false --dart-define TEST_FEATURE_PLAYBACK_RATE=false - - android-exo: - runs-on: ubuntu-latest - timeout-minutes: 90 - if: inputs.enable_android - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ inputs.flutter_version }} - channel: ${{ inputs.flutter_channel }} - - name: Override endorsed plugin with audioplayers_android_exo - run: | - dart pub add "audioplayers_android_exo:{path: ../../audioplayers_android_exo}" - working-directory: ./packages/audioplayers/example - - uses: bluefireteam/melos-action@v3 - - name: Setup Android Emulator - timeout-minutes: 10 - run: bash ./scripts/ci/setup-android.sh 35 - - name: Run Flutter integration tests - working-directory: ./packages/audioplayers/example - # Need to execute lib and app tests one by one, see: https://github.com/flutter/flutter/issues/101031 - run: | - ( cd server; dart run bin/server.dart ) & - flutter test integration_test/platform_test.dart --dart-define USE_LOCAL_SERVER=true --dart-define TEST_FEATURE_LOW_LATENCY=false - flutter test integration_test/lib_test.dart --dart-define USE_LOCAL_SERVER=true --dart-define TEST_FEATURE_LOW_LATENCY=false - flutter test integration_test/app_test.dart --dart-define USE_LOCAL_SERVER=true --dart-define TEST_FEATURE_LOW_LATENCY=false - - android: - runs-on: ubuntu-latest - timeout-minutes: 90 - if: inputs.enable_android - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ inputs.flutter_version }} - channel: ${{ inputs.flutter_channel }} - - uses: bluefireteam/melos-action@v3 - - name: Setup Android Emulator - timeout-minutes: 10 - run: bash ./scripts/ci/setup-android.sh 35 - - name: Run Flutter integration tests - working-directory: ./packages/audioplayers/example - # Need to execute lib and app tests one by one, see: https://github.com/flutter/flutter/issues/101031 - run: | - ( cd server; dart run bin/server.dart ) & - flutter test integration_test/platform_test.dart --dart-define USE_LOCAL_SERVER=true - flutter test integration_test/lib_test.dart --dart-define USE_LOCAL_SERVER=true - flutter test integration_test/app_test.dart --dart-define USE_LOCAL_SERVER=true - - name: Run Android unit tests - working-directory: ./packages/audioplayers/example/android - # TODO: Use `./gradlew test`, when https://github.com/flutter/flutter/issues/169336 is fixed. - run: ./gradlew testDebugUnitTest - - ios-min: - # Run lib tests only to ensure compatibility with iOS 16. - runs-on: macos-13 - timeout-minutes: 60 - if: inputs.enable_min_version && inputs.enable_ios - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ inputs.flutter_version }} - channel: ${{ inputs.flutter_channel }} - - uses: bluefireteam/melos-action@v3 - - - name: Run Flutter integration tests - working-directory: ./packages/audioplayers/example - run: | - sudo xcode-select -switch /Applications/Xcode_14.3.1.app/Contents/Developer - UDID=$(xcrun simctl create test-se-16-4 com.apple.CoreSimulator.SimDeviceType.iPhone-SE-3rd-generation com.apple.CoreSimulator.SimRuntime.iOS-16-4) - xcrun simctl list devices - echo "Using simulator $UDID" - xcrun simctl boot "${UDID:?No Simulator with this name iPhone found}" - sudo xcode-select -switch /Applications/Xcode_15.2.app/Contents/Developer - ( cd server; dart run bin/server.dart ) & - flutter test -d $UDID integration_test/lib_test.dart --dart-define USE_LOCAL_SERVER=true - - ios: - runs-on: macos-14 - timeout-minutes: 60 - if: inputs.enable_ios - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ inputs.flutter_version }} - channel: ${{ inputs.flutter_channel }} - - run: flutter config --enable-swift-package-manager - - uses: bluefireteam/melos-action@main - - - name: Run Flutter integration tests - working-directory: ./packages/audioplayers/example - # Need to execute lib and app tests one by one, see: https://github.com/flutter/flutter/issues/101031 - run: | - UDID=$(xcrun simctl create test-se-17-2 com.apple.CoreSimulator.SimDeviceType.iPhone-SE-3rd-generation com.apple.CoreSimulator.SimRuntime.iOS-17-2) - xcrun simctl list devices - echo "Using simulator $UDID" - xcrun simctl boot "${UDID:?No Simulator with this name iPhone found}" - ( cd server; dart run bin/server.dart ) & - flutter test -d $UDID integration_test/platform_test.dart --dart-define USE_LOCAL_SERVER=true - flutter test -d $UDID integration_test/lib_test.dart --dart-define USE_LOCAL_SERVER=true - flutter test -d $UDID integration_test/app_test.dart --dart-define USE_LOCAL_SERVER=true - - # Remove as soon as support for cocoapods is removed at Flutter - ios-pods: - runs-on: macos-14 - timeout-minutes: 30 - if: inputs.enable_ios - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ inputs.flutter_version }} - channel: ${{ inputs.flutter_channel }} - - run: flutter config --no-enable-swift-package-manager - - uses: bluefireteam/melos-action@v3 - - - name: Example app - Build iOS - working-directory: ./packages/audioplayers/example - run: flutter build ios --release --no-codesign - - macos-min: - runs-on: macos-13 - timeout-minutes: 30 - if: inputs.enable_min_version && inputs.enable_macos - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ inputs.flutter_version }} - channel: ${{ inputs.flutter_channel }} - - uses: bluefireteam/melos-action@main - - - name: Run Flutter integration tests - working-directory: ./packages/audioplayers/example - # Need to execute lib and app tests one by one, see: https://github.com/flutter/flutter/issues/101031 - run: | - ( cd server; dart run bin/server.dart ) & - flutter test -d macos integration_test/lib_test.dart --dart-define USE_LOCAL_SERVER=true - - macos: - runs-on: macos-14 - timeout-minutes: 30 - if: inputs.enable_macos - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ inputs.flutter_version }} - channel: ${{ inputs.flutter_channel }} - - run: flutter config --enable-swift-package-manager - - uses: bluefireteam/melos-action@v3 - - - name: Run Flutter integration tests - working-directory: ./packages/audioplayers/example - # Need to execute lib and app tests one by one, see: https://github.com/flutter/flutter/issues/101031 - run: | - ( cd server; dart run bin/server.dart ) & - flutter test -d macos integration_test/platform_test.dart --dart-define USE_LOCAL_SERVER=true - flutter test -d macos integration_test/lib_test.dart --dart-define USE_LOCAL_SERVER=true - flutter test -d macos integration_test/app_test.dart --dart-define USE_LOCAL_SERVER=true - - # Remove as soon as support for cocoapods is removed at Flutter - macos-pods: - runs-on: macos-14 - timeout-minutes: 30 - if: inputs.enable_macos - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ inputs.flutter_version }} - channel: ${{ inputs.flutter_channel }} - - run: flutter config --no-enable-swift-package-manager - - uses: bluefireteam/melos-action@v3 - - - name: Example app - Build macOS - working-directory: ./packages/audioplayers/example - run: flutter build macos --release - - windows: - runs-on: windows-latest - timeout-minutes: 30 - if: inputs.enable_windows - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ inputs.flutter_version }} - channel: ${{ inputs.flutter_channel }} - - uses: bluefireteam/melos-action@v3 - - name: Download virtual audio device - # Download has to be done before setting the system date time. - timeout-minutes: 1 - run: | - Invoke-WebRequest https://github.com/duncanthrax/scream/releases/download/4.0/Scream4.0.zip -OutFile Scream.zip - Expand-Archive -Path Scream.zip -DestinationPath Scream - - name: Disable time sync with Hyper-V & setting system date time (#1573) - # TODO(gustl22): Remove workaround of setting the time when virtual audio device certificate is valid again (#1573) - run: | - Set-Service -Name vmictimesync -Status stopped -StartupType disabled - Set-ItemProperty HKLM:\SYSTEM\CurrentControlSet\services\W32Time\Parameters -Name 'Type' -Value 'NoSync' - net stop w32time; Set-Date (Get-Date "2023-07-04 12:00:00") - - name: Install virtual audio device - timeout-minutes: 1 - run: | - Import-Certificate -FilePath Scream\Install\driver\x64\Scream.cat -CertStoreLocation Cert:\LocalMachine\TrustedPublisher - Scream\Install\helpers\devcon-x64.exe install Scream\Install\driver\x64\Scream.inf *Scream - - name: Resetting system date time (#1573) - run: | - Set-Service -Name vmictimesync -Status running -StartupType automatic - Set-ItemProperty HKLM:\SYSTEM\CurrentControlSet\services\W32Time\Parameters -Name 'Type' -Value 'NTP' - net start w32time; w32tm /resync /force; $currentDate = Get-Date; Write-Host "Current System Date: $currentDate"; - - name: Run Flutter integration tests - working-directory: ./packages/audioplayers/example - shell: bash # Needed in order to fail fast, see: https://github.com/actions/runner-images/issues/6668 - # Need to execute lib and app tests one by one, see: https://github.com/flutter/flutter/issues/101031 - run: | - ( cd server; dart run bin/server.dart ) & - flutter test -d windows integration_test/platform_test.dart --dart-define USE_LOCAL_SERVER=true - flutter test -d windows integration_test/lib_test.dart --dart-define USE_LOCAL_SERVER=true - flutter test -d windows integration_test/app_test.dart --dart-define USE_LOCAL_SERVER=true - - linux: - runs-on: ubuntu-latest - timeout-minutes: 30 - if: inputs.enable_linux - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ inputs.flutter_version }} - channel: ${{ inputs.flutter_channel }} - - uses: bluefireteam/melos-action@v3 - - name: Install Flutter requirements for Linux - run: | - sudo apt-get update - sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev - - name: Install GStreamer - # Install libunwind-dev, see https://github.com/actions/runner-images/issues/6399#issuecomment-1285011525 - run: | - sudo apt install -y libunwind-dev - sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-good gstreamer1.0-plugins-bad - - - name: Run Flutter integration tests - working-directory: ./packages/audioplayers/example - # Need to execute lib and app tests one by one, see: https://github.com/flutter/flutter/issues/101031 - # TODO(gustl22): Linux tests are flaky with LIVE_MODE=false - run: | - export DISPLAY=:99 - sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & - ( cd server; LIVE_MODE=true dart run bin/server.dart ) & - flutter test -d linux integration_test/platform_test.dart --dart-define USE_LOCAL_SERVER=true - flutter test -d linux integration_test/lib_test.dart --dart-define USE_LOCAL_SERVER=true - flutter test -d linux integration_test/app_test.dart --dart-define USE_LOCAL_SERVER=true diff --git a/.gitignore b/.gitignore index 5f831f935..c3d7b8877 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,11 @@ pubspec.lock .dart_tool/ pubspec_overrides.yaml .flutter-plugins-dependencies + +# AI Assistants +CLAUDE.md +GEMINI.md +COMMIT_MESSAGE.md +GIT_COMMIT_MSG.txt +.claude/ +.gemini/ \ No newline at end of file diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 000000000..fa0b357c4 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..21406f673 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "audioplayers", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/packages/audioplayers/example/.metadata b/packages/audioplayers/example/.metadata index af5cda898..83b34ebbb 100644 --- a/packages/audioplayers/example/.metadata +++ b/packages/audioplayers/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "edada7c56edf4a183c1735310e123c7f923584f1" + revision: "f6ff1529fd6d8af5f706051d9251ac9231c83407" channel: "stable" project_type: app @@ -13,26 +13,26 @@ project_type: app migration: platforms: - platform: root - create_revision: edada7c56edf4a183c1735310e123c7f923584f1 - base_revision: edada7c56edf4a183c1735310e123c7f923584f1 + create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - platform: android - create_revision: edada7c56edf4a183c1735310e123c7f923584f1 - base_revision: edada7c56edf4a183c1735310e123c7f923584f1 + create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - platform: ios - create_revision: edada7c56edf4a183c1735310e123c7f923584f1 - base_revision: edada7c56edf4a183c1735310e123c7f923584f1 + create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - platform: linux - create_revision: edada7c56edf4a183c1735310e123c7f923584f1 - base_revision: edada7c56edf4a183c1735310e123c7f923584f1 + create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - platform: macos - create_revision: edada7c56edf4a183c1735310e123c7f923584f1 - base_revision: edada7c56edf4a183c1735310e123c7f923584f1 + create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - platform: web - create_revision: edada7c56edf4a183c1735310e123c7f923584f1 - base_revision: edada7c56edf4a183c1735310e123c7f923584f1 + create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - platform: windows - create_revision: edada7c56edf4a183c1735310e123c7f923584f1 - base_revision: edada7c56edf4a183c1735310e123c7f923584f1 + create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 # User provided section diff --git a/packages/audioplayers/example/android/app/src/main/kotlin/xyz/luan/audioplayers/audioplayers_example/MainActivity.kt b/packages/audioplayers/example/android/app/src/main/kotlin/xyz/luan/audioplayers/audioplayers_example/MainActivity.kt new file mode 100644 index 000000000..baec57ca6 --- /dev/null +++ b/packages/audioplayers/example/android/app/src/main/kotlin/xyz/luan/audioplayers/audioplayers_example/MainActivity.kt @@ -0,0 +1,5 @@ +package xyz.luan.audioplayers.audioplayers_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/packages/audioplayers/example/android/build.gradle.kts b/packages/audioplayers/example/android/build.gradle.kts index dbee657bb..c3314db59 100644 --- a/packages/audioplayers/example/android/build.gradle.kts +++ b/packages/audioplayers/example/android/build.gradle.kts @@ -5,16 +5,12 @@ allprojects { } } -val newBuildDir: Directory = - rootProject.layout.buildDirectory - .dir("../../build") - .get() -rootProject.layout.buildDirectory.value(newBuildDir) +rootProject.layout.buildDirectory = rootProject.layout.projectDirectory.dir("../build") subprojects { - val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) - project.layout.buildDirectory.value(newSubprojectBuildDir) + project.layout.buildDirectory = rootProject.layout.buildDirectory.dir(project.name).get() } + subprojects { project.evaluationDependsOn(":app") } diff --git a/packages/audioplayers/example/android/gradle.properties b/packages/audioplayers/example/android/gradle.properties index fbee1d8cd..4685b2486 100644 --- a/packages/audioplayers/example/android/gradle.properties +++ b/packages/audioplayers/example/android/gradle.properties @@ -1,2 +1,7 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true + +# Disable Kotlin incremental compilation to fix cross-drive path issues +kotlin.incremental=false +kotlin.incremental.js=false +kotlin.incremental.multiplatform=false diff --git a/packages/audioplayers/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/audioplayers/example/android/gradle/wrapper/gradle-wrapper.properties index ac3b47926..02767eb1c 100644 --- a/packages/audioplayers/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/audioplayers/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-8.12-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip diff --git a/packages/audioplayers/example/android/settings.gradle.kts b/packages/audioplayers/example/android/settings.gradle.kts index fb605bc84..0c550df01 100644 --- a/packages/audioplayers/example/android/settings.gradle.kts +++ b/packages/audioplayers/example/android/settings.gradle.kts @@ -19,7 +19,7 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.9.1" apply false + id("com.android.application") version "8.13.2" apply false id("org.jetbrains.kotlin.android") version "2.1.0" apply false } diff --git a/packages/audioplayers/example/integration_test/app/app_source_test_data.dart b/packages/audioplayers/example/integration_test/app/app_source_test_data.dart deleted file mode 100644 index 1723ce151..000000000 --- a/packages/audioplayers/example/integration_test/app/app_source_test_data.dart +++ /dev/null @@ -1,80 +0,0 @@ -import '../platform_features.dart'; -import '../source_test_data.dart'; - -/// Data of a ui test source. -class AppSourceTestData extends SourceTestData { - String sourceKey; - - AppSourceTestData({ - required this.sourceKey, - required super.duration, - super.isVBR, - }); - - @override - String toString() { - return 'UiSourceTestData(' - 'sourceKey: $sourceKey, ' - 'duration: $duration, ' - 'isVBR: $isVBR' - ')'; - } -} - -final _features = PlatformFeatures.instance(); - -// All sources are tested again in lib or platform tests, -// therefore comment most of them to save testing time -final audioTestDataList = [ - if (_features.hasUrlSource) - AppSourceTestData( - sourceKey: 'url-remote-wav-1', - duration: const Duration(milliseconds: 451), - ), - /*if (_features.hasUrlSource) - AppSourceTestData( - sourceKey: 'url-remote-wav-2', - duration: const Duration(seconds: 1, milliseconds: 068), - ),*/ - /*if (_features.hasUrlSource) - AppSourceTestData( - sourceKey: 'url-remote-mp3-1', - isVBR: true, - duration: const Duration(minutes: 3, seconds: 30, milliseconds: 77), - ),*/ - /*if (_features.hasUrlSource) - AppSourceTestData( - sourceKey: 'url-remote-mp3-2', - duration: const Duration(minutes: 1, seconds: 34, milliseconds: 119), - ),*/ - if (_features.hasUrlSource && _features.hasPlaylistSourceType) - AppSourceTestData( - sourceKey: 'url-remote-m3u8', - duration: null, - ), - /*if (_features.hasUrlSource) - AppSourceTestData( - sourceKey: 'url-remote-mpga', - duration: null, - ),*/ - /*if (_features.hasAssetSource) - AppSourceTestData( - sourceKey: 'asset-wav', - duration: const Duration(seconds: 1, milliseconds: 068), - ),*/ - /*if (_features.hasAssetSource) - AppSourceTestData( - sourceKey: 'asset-mp3', - duration: const Duration(minutes: 1, seconds: 34, milliseconds: 119), - ),*/ - /*if (_features.hasBytesSource) - AppSourceTestData( - sourceKey: 'bytes-local', - duration: const Duration(seconds: 1, milliseconds: 068), - ),*/ - /*if (_features.hasBytesSource) - AppSourceTestData( - sourceKey: 'bytes-remote', - duration: const Duration(minutes: 3, seconds: 30, milliseconds: 76), - ),*/ -]; diff --git a/packages/audioplayers/example/integration_test/app/app_test_utils.dart b/packages/audioplayers/example/integration_test/app/app_test_utils.dart deleted file mode 100644 index d8f56acc9..000000000 --- a/packages/audioplayers/example/integration_test/app/app_test_utils.dart +++ /dev/null @@ -1,206 +0,0 @@ -import 'dart:async'; - -import 'package:audioplayers_example/components/tgl.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; - -extension AppWidgetTester on WidgetTester { - /// Wait until appearance and disappearance - Future waitOneshot( - Key key, { - Duration timeout = const Duration(seconds: 180), - String? stackTrace, - }) async { - await waitFor( - () async => expect( - find.byKey(key), - findsOneWidget, - ), - timeout: timeout, - pollInterval: const Duration(milliseconds: 100), - stackTrace: stackTrace, - ); - await waitFor( - () async => expect( - find.byKey(key), - findsNothing, - ), - stackTrace: stackTrace, - ); - } - - // Add [stackTrace] to work around https://github.com/flutter/flutter/issues/89138 - Future waitFor( - Future Function() testExpectation, { - Duration? timeout = const Duration(seconds: 15), - Duration? pollInterval = const Duration(milliseconds: 500), - String? stackTrace, - }) async => - _waitUntil( - (setFailureMessage) async { - try { - await pump(); - await testExpectation(); - return true; - } on TestFailure catch (e) { - setFailureMessage(e.message ?? ''); - return false; - } - }, - timeout: timeout, - pollInterval: pollInterval, - stackTrace: stackTrace, - ); - - /// Waits until the [condition] returns true - /// Will raise a complete with a [TimeoutException] if the - /// condition does not return true with the timeout period. - /// Copied from: https://github.com/jonsamwell/flutter_gherkin/blob/02a4af91d7a2512e0a4540b9b1ab13e36d5c6f37/lib/src/flutter/utils/driver_utils.dart#L86 - Future _waitUntil( - Future Function(void Function(String message) setFailureMessage) - condition, { - Duration? timeout = const Duration(seconds: 15), - Duration? pollInterval = const Duration(milliseconds: 500), - String? stackTrace, - }) async { - var firstFailureMsg = ''; - var lastFailureMsg = 'same as first failure'; - void setFailureMessage(String message) { - if (firstFailureMsg.isEmpty) { - firstFailureMsg = '${DateTime.now()}:\n $message'; - } else { - lastFailureMsg = '${DateTime.now()}:\n $message'; - } - } - - try { - await Future.microtask( - () async { - final completer = Completer(); - final maxAttempts = - (timeout!.inMilliseconds / pollInterval!.inMilliseconds).round(); - var attempts = 0; - - while (attempts < maxAttempts) { - final result = await condition(setFailureMessage); - if (result) { - completer.complete(); - break; - } else { - await Future.delayed(pollInterval); - } - attempts++; - } - }, - ).timeout( - timeout!, - ); - } on TimeoutException catch (e) { - throw Exception( - '''$e - -Stacktrace: -$stackTrace -First Failure: -$firstFailureMsg -Last Failure: -$lastFailureMsg''', - ); - } - } - - Future scrollToAndTap(Key widgetKey) async { - await scrollTo(widgetKey); - await tap(find.byKey(widgetKey)); - } - - Future scrollTo(Key widgetKey) async { - final finder = find.byKey(widgetKey); - if (finder.hitTestable().evaluate().isEmpty) { - await scrollUntilVisible( - finder, - 100, - scrollable: find.byType(Scrollable).first, - ); - } - await pump(); - } -} - -void expectWidgetHasText( - Key key, { - required Matcher matcher, - bool skipOffstage = true, -}) { - final widget = - find.byKey(key, skipOffstage: skipOffstage).evaluate().single.widget; - if (widget is Text) { - expect(widget.data, matcher); - } else { - throw 'Widget with key $key is not a Widget of type "Text"'; - } -} - -void expectWidgetHasDuration( - Key key, { - required dynamic matcher, - bool skipOffstage = true, -}) { - final widget = - find.byKey(key, skipOffstage: skipOffstage).evaluate().single.widget; - if (widget is Text) { - final regexp = RegExp(r'\d+:\d{2}:\d{2}.\d{6}'); - final match = regexp.firstMatch(widget.data ?? ''); - final duration = _parseDuration(match?.group(0)); - expect(duration, matcher); - } else { - throw 'Widget with key $key is not a Widget of type "Text"'; - } -} - -/// Parse Duration string to Duration -Duration? _parseDuration(String? s) { - if (s == null || s.isEmpty) { - return null; - } - var hours = 0; - var minutes = 0; - var micros = 0; - final parts = s.split(':'); - if (parts.length > 2) { - hours = int.parse(parts[parts.length - 3]); - } - if (parts.length > 1) { - minutes = int.parse(parts[parts.length - 2]); - } - micros = (double.parse(parts[parts.length - 1]) * 1000000).round(); - return Duration(hours: hours, minutes: minutes, microseconds: micros); -} - -void expectEnumToggleHasSelected( - Key key, { - required Matcher matcher, - bool skipOffstage = true, -}) { - final widget = - find.byKey(key, skipOffstage: skipOffstage).evaluate().single.widget; - if (widget is EnumTgl) { - expect(widget.selected, matcher); - } else { - throw 'Widget with key $key is not a Widget of type "EnumTgl"'; - } -} - -void expectToggleHasSelected( - Key key, { - required Matcher matcher, - bool skipOffstage = true, -}) { - final widget = - find.byKey(key, skipOffstage: skipOffstage).evaluate().single.widget; - if (widget is Tgl) { - expect(widget.selected, matcher); - } else { - throw 'Widget with key $key is not a Widget of type "Tgl"'; - } -} diff --git a/packages/audioplayers/example/integration_test/app/tabs/context_tab.dart b/packages/audioplayers/example/integration_test/app/tabs/context_tab.dart deleted file mode 100644 index 4d69a2dcb..000000000 --- a/packages/audioplayers/example/integration_test/app/tabs/context_tab.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -import '../../platform_features.dart'; -import '../../test_utils.dart'; -import '../app_source_test_data.dart'; - -Future testContextTab( - WidgetTester tester, - AppSourceTestData audioSourceTestData, - PlatformFeatures features, -) async { - printWithTimeOnFailure('Test Context Tab'); - // Audio context - // TODO(Gustl22): test generic flags - // await tester.tap(find.byKey(const Key('audioContextTab'))); - // await tester.pumpAndSettle(); -} diff --git a/packages/audioplayers/example/integration_test/app/tabs/controls_tab.dart b/packages/audioplayers/example/integration_test/app/tabs/controls_tab.dart deleted file mode 100644 index c26d6fb61..000000000 --- a/packages/audioplayers/example/integration_test/app/tabs/controls_tab.dart +++ /dev/null @@ -1,253 +0,0 @@ -import 'package:audioplayers/audioplayers.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../../platform_features.dart'; -import '../../test_utils.dart'; -import '../app_source_test_data.dart'; -import '../app_test_utils.dart'; -import 'properties.dart'; -import 'source_tab.dart'; - -Future testControlsTab( - WidgetTester tester, - AppSourceTestData audioSourceTestData, - PlatformFeatures features, -) async { - printWithTimeOnFailure('Test Controls Tab'); - await tester.tap(find.byKey(const Key('controlsTab'))); - await tester.pumpAndSettle(); - - // Sources take some time to get initialized - const stopDuration = Duration(seconds: 5); - - if (features.hasVolume) { - await tester.testVolume('0.5', stopDuration: stopDuration); - await tester.testVolume('0.0', stopDuration: stopDuration); - await tester.testVolume('1.0', stopDuration: stopDuration); - // No tests for volume > 1 - } - - if (features.hasBalance) { - await tester.testBalance('-1.0', stopDuration: stopDuration); - await tester.testBalance('1.0', stopDuration: stopDuration); - await tester.testBalance('0.0', stopDuration: stopDuration); - } - - if (features.hasPlaybackRate && !audioSourceTestData.isLiveStream) { - // TODO(Gustl22): also test for playback rate in streams - await tester.testRate('0.5'); - await tester.testRate('2.0'); - await tester.testRate('1.0'); - } - - if (features.hasSeek && !audioSourceTestData.isLiveStream) { - // TODO(Gustl22): also test seeking in streams - final isImmediateDurationSupported = features.hasMp3Duration || - !audioSourceTestData.sourceKey.contains('mp3'); - - // Linux cannot complete seek if duration is not present. - await tester.testSeek('0.5', isResume: false); - await tester.doInStreamsTab((tester) async { - if (isImmediateDurationSupported) { - await tester.testPosition( - Duration(seconds: audioSourceTestData.duration!.inSeconds ~/ 2), - matcher: (Object? value) => - greaterThanOrEqualTo(value ?? Duration.zero), - ); - } - }); - - await tester.pump(const Duration(seconds: 1)); - await tester.testSeek('1.0'); - await tester.pump(const Duration(seconds: 1)); - await tester.stop(); - } - - // Test all features in low latency mode: - final isBytesSource = audioSourceTestData.sourceKey.contains('bytes'); - if (features.hasLowLatency && - !audioSourceTestData.isLiveStream && - !isBytesSource) { - await tester.testPlayerMode(PlayerMode.lowLatency); - - // Test resume - await tester.resume(); - await tester.pump(const Duration(seconds: 1)); - // Test pause - await tester.scrollToAndTap(const Key('control-pause')); - await tester.resume(); - await tester.pump(const Duration(seconds: 1)); - await tester.stop(); - - // Test volume - await tester.testVolume('0.5'); - await tester.testVolume('1.0'); - - // Test release mode: loop - await tester.testReleaseMode(ReleaseMode.loop); - await tester.pump(const Duration(seconds: 3)); - await tester.stop(); - await tester.testReleaseMode(ReleaseMode.stop, isResume: false); - await tester.pumpAndSettle(); - - // Reset to media player - await tester.testPlayerMode(PlayerMode.mediaPlayer); - await tester.pumpAndSettle(); - } - - if (!audioSourceTestData.isLiveStream && - audioSourceTestData.duration! < const Duration(seconds: 2)) { - final isAndroid = - !kIsWeb && defaultTargetPlatform == TargetPlatform.android; - // FIXME(gustl22): Android provides no position for samples shorter - // than 0.5 seconds. - if (features.hasReleaseModeLoop && - !(isAndroid && - audioSourceTestData.duration! < const Duration(seconds: 1))) { - await tester.testReleaseMode(ReleaseMode.loop); - await tester.pump(const Duration(seconds: 3)); - // Check if sound has started playing. - await tester.doInStreamsTab((tester) async { - await tester.testPosition( - Duration.zero, - matcher: (Duration? position) => - greaterThan(position ?? Duration.zero), - ); - }); - await tester.stop(); - await tester.testReleaseMode(ReleaseMode.stop, isResume: false); - await tester.pumpAndSettle(); - } - - if (features.hasReleaseModeRelease) { - await tester.testReleaseMode(ReleaseMode.release); - await tester.pump(const Duration(seconds: 3)); - // No need to call stop, as it should be released by now. - // Ensure source was released by checking `position == null`. - await tester.doInStreamsTab((tester) async { - await tester.testPosition(null); - }); - - // Reinitialize source - await tester.tap(find.byKey(const Key('sourcesTab'))); - await tester.pumpAndSettle(); - await tester.testSource(audioSourceTestData.sourceKey); - - await tester.tap(find.byKey(const Key('controlsTab'))); - await tester.pumpAndSettle(); - - await tester.testReleaseMode(ReleaseMode.stop, isResume: false); - await tester.pumpAndSettle(); - - // TODO(Gustl22): test 'control-release' - } - } -} - -extension ControlsWidgetTester on WidgetTester { - Future resume() async { - await scrollToAndTap(const Key('control-resume')); - await pump(); - } - - Future stop() async { - final st = StackTrace.current.toString(); - - await scrollToAndTap(const Key('control-stop')); - await waitOneshot(const Key('toast-player-stopped-0'), stackTrace: st); - await pump(); - } - - Future testVolume( - String volume, { - Duration stopDuration = const Duration(seconds: 1), - }) async { - printWithTimeOnFailure('Test Volume: $volume'); - await scrollToAndTap(Key('control-volume-$volume')); - await resume(); - await pump(stopDuration); - await stop(); - } - - Future testBalance( - String balance, { - Duration stopDuration = const Duration(seconds: 1), - }) async { - printWithTimeOnFailure('Test Balance: $balance'); - await scrollToAndTap(Key('control-balance-$balance')); - await resume(); - await pump(stopDuration); - await stop(); - } - - Future testRate( - String rate, { - Duration stopDuration = const Duration(seconds: 2), - }) async { - printWithTimeOnFailure('Test Rate: $rate'); - await scrollToAndTap(Key('control-rate-$rate')); - await resume(); - await pump(stopDuration); - await stop(); - } - - Future testSeek( - String seek, { - bool isResume = true, - }) async { - printWithTimeOnFailure('Test Seek: $seek'); - final st = StackTrace.current.toString(); - - await scrollToAndTap(Key('control-seek-$seek')); - - await waitOneshot(const Key('toast-seek-complete-0'), stackTrace: st); - - if (isResume) { - await resume(); - } - } - - Future testPlayerMode(PlayerMode mode) async { - printWithTimeOnFailure('Test Player Mode: ${mode.name}'); - final st = StackTrace.current.toString(); - - await scrollToAndTap(Key('control-player-mode-${mode.name}')); - await waitFor( - () async => expectEnumToggleHasSelected( - const Key('control-player-mode'), - matcher: equals(mode), - ), - stackTrace: st, - ); - } - - Future testReleaseMode(ReleaseMode mode, {bool isResume = true}) async { - printWithTimeOnFailure('Test Release Mode: ${mode.name}'); - final st = StackTrace.current.toString(); - - await scrollToAndTap(Key('control-release-mode-${mode.name}')); - await waitFor( - () async => expectEnumToggleHasSelected( - const Key('control-release-mode'), - matcher: equals(mode), - ), - stackTrace: st, - ); - if (isResume) { - await resume(); - } - } - - Future doInStreamsTab( - Future Function(WidgetTester tester) foo, - ) async { - await tap(find.byKey(const Key('streamsTab'))); - await pump(); - - await foo(this); - - await tap(find.byKey(const Key('controlsTab'))); - await pump(); - } -} diff --git a/packages/audioplayers/example/integration_test/app/tabs/logs_tab.dart b/packages/audioplayers/example/integration_test/app/tabs/logs_tab.dart deleted file mode 100644 index a77be8ff6..000000000 --- a/packages/audioplayers/example/integration_test/app/tabs/logs_tab.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -import '../../platform_features.dart'; -import '../../test_utils.dart'; -import '../app_source_test_data.dart'; - -Future testLogsTab( - WidgetTester tester, - AppSourceTestData audioSourceTestData, - PlatformFeatures features, -) async { - printWithTimeOnFailure('Test Logs Tab'); - // TODO(Gustl22): may test logs - // await tester.tap(find.byKey(const Key('loggerTab'))); - // await tester.pumpAndSettle(); -} diff --git a/packages/audioplayers/example/integration_test/app/tabs/properties.dart b/packages/audioplayers/example/integration_test/app/tabs/properties.dart deleted file mode 100644 index 8c451f898..000000000 --- a/packages/audioplayers/example/integration_test/app/tabs/properties.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:audioplayers/audioplayers.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../../test_utils.dart'; -import '../app_test_utils.dart'; - -extension PropertiesWidgetTester on WidgetTester { - Future testDuration( - Duration? duration, { - Duration timeout = const Duration(seconds: 4), - }) async { - printWithTimeOnFailure('Test Duration: $duration'); - final st = StackTrace.current.toString(); - await waitFor( - () async { - await scrollToAndTap(const Key('refreshButton')); - await pump(); - expectWidgetHasDuration( - const Key('durationText'), - // TODO(gustl22): once duration is always null for streams, - // then can remove fallback for Duration.zero - matcher: (Duration? actual) => durationRangeMatcher( - actual ?? Duration.zero, - duration ?? Duration.zero, - ), - ); - }, - timeout: timeout, - stackTrace: st, - ); - } - - Future testPosition( - Duration? position, { - Matcher Function(Duration?) matcher = equals, - Duration timeout = const Duration(seconds: 4), - }) async { - printWithTimeOnFailure('Test Position: $position'); - final st = StackTrace.current.toString(); - await waitFor( - () async { - await scrollToAndTap(const Key('refreshButton')); - await pump(); - expectWidgetHasDuration( - const Key('positionText'), - matcher: matcher(position), - ); - }, - timeout: timeout, - stackTrace: st, - ); - } - - Future testPlayerState( - PlayerState playerState, { - Duration timeout = const Duration(seconds: 4), - }) async { - printWithTimeOnFailure('Test PlayerState: $playerState'); - final st = StackTrace.current.toString(); - await waitFor( - () async { - await scrollToAndTap(const Key('refreshButton')); - await pump(); - expectWidgetHasText( - const Key('playerStateText'), - matcher: contains(playerState.toString()), - ); - }, - timeout: timeout, - stackTrace: st, - ); - } -} diff --git a/packages/audioplayers/example/integration_test/app/tabs/source_tab.dart b/packages/audioplayers/example/integration_test/app/tabs/source_tab.dart deleted file mode 100644 index f8dac11b0..000000000 --- a/packages/audioplayers/example/integration_test/app/tabs/source_tab.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../../platform_features.dart'; -import '../../test_utils.dart'; -import '../app_source_test_data.dart'; -import '../app_test_utils.dart'; - -Future testSourcesTab( - WidgetTester tester, - AppSourceTestData audioSourceTestData, - PlatformFeatures features, -) async { - printWithTimeOnFailure('Test Sources Tab'); - await tester.tap(find.byKey(const Key('sourcesTab'))); - await tester.pumpAndSettle(); - - await tester.testSource(audioSourceTestData.sourceKey); -} - -extension ControlsWidgetTester on WidgetTester { - Future testSource(String sourceKey) async { - printWithTimeOnFailure('Test setting source: $sourceKey'); - final st = StackTrace.current.toString(); - final sourceWidgetKey = Key('setSource-$sourceKey'); - await scrollToAndTap(sourceWidgetKey); - - await waitOneshot(const Key('toast-set-source'), stackTrace: st); - } -} diff --git a/packages/audioplayers/example/integration_test/app/tabs/stream_tab.dart b/packages/audioplayers/example/integration_test/app/tabs/stream_tab.dart deleted file mode 100644 index 4b2beca60..000000000 --- a/packages/audioplayers/example/integration_test/app/tabs/stream_tab.dart +++ /dev/null @@ -1,187 +0,0 @@ -import 'package:audioplayers/audioplayers.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../../platform_features.dart'; -import '../../test_utils.dart'; -import '../app_source_test_data.dart'; -import '../app_test_utils.dart'; -import 'properties.dart'; - -Future testStreamsTab( - WidgetTester tester, - AppSourceTestData audioSourceTestData, - PlatformFeatures features, -) async { - printWithTimeOnFailure('Test Streams Tab'); - await tester.tap(find.byKey(const Key('streamsTab'))); - await tester.pumpAndSettle(); - - // Stream position is tracked as soon as source is loaded - // FIXME: Flaky position test for web, remove kIsWeb check. - if (!kIsWeb && !audioSourceTestData.isLiveStream) { - // Display position before playing - await tester.testPosition(Duration.zero); - } - - if (features.hasDurationEvent && !audioSourceTestData.isVBR) { - // Display duration before playing - await tester.testDuration(audioSourceTestData.duration); - } - - // Sources take some time to get initialized - const timeout = Duration(seconds: 8); - - await tester.pumpAndSettle(); - await tester.scrollToAndTap(const Key('play_button')); - await tester.pump(); - - // Cannot test more precisely as it is dependent on pollInterval - // and updateInterval of native implementation. - if (audioSourceTestData.isLiveStream || - audioSourceTestData.duration! > const Duration(seconds: 2)) { - // Test player state: playing - if (features.hasPlayerStateEvent) { - // Only test, if there's enough time to be able to check playing state. - await tester.testPlayerState(PlayerState.playing, timeout: timeout); - await tester.testOnPlayerState(PlayerState.playing, timeout: timeout); - } - - // Test if onPositionText is set. - await tester.testPosition( - Duration.zero, - matcher: (Duration? position) => greaterThan(position ?? Duration.zero), - timeout: timeout, - ); - await tester.testOnPosition( - Duration.zero, - matcher: greaterThan, - timeout: timeout, - ); - } - - if (features.hasDurationEvent && !audioSourceTestData.isLiveStream) { - // Test if onDurationText is set. - await tester.testOnDuration( - audioSourceTestData.duration!, - timeout: timeout, - ); - } - - const sampleDuration = Duration(seconds: 3); - await tester.pump(sampleDuration); - - // Test player states: pause, stop, completed - if (features.hasPlayerStateEvent) { - if (!audioSourceTestData.isLiveStream) { - if (audioSourceTestData.duration! < const Duration(seconds: 2)) { - await tester.testPlayerState(PlayerState.completed, timeout: timeout); - await tester.testOnPlayerState(PlayerState.completed, timeout: timeout); - } else if (audioSourceTestData.duration! > const Duration(seconds: 5)) { - await tester.scrollToAndTap(const Key('pause_button')); - await tester.pumpAndSettle(); - await tester.testPlayerState(PlayerState.paused); - await tester.testOnPlayerState(PlayerState.paused); - - await tester.stopStream(); - await tester.testPlayerState(PlayerState.stopped); - await tester.testOnPlayerState(PlayerState.stopped); - } else { - // Cannot say for sure, if it's stopped or completed, so we just stop - await tester.stopStream(); - } - } else { - await tester.stopStream(); - await tester.testPlayerState(PlayerState.stopped, timeout: timeout); - await tester.testOnPlayerState(PlayerState.stopped, timeout: timeout); - } - } - - // Display duration & position after completion / stop - // FIXME(Gustl22): Linux does not support duration after completion event - if (features.hasDurationEvent && - (kIsWeb || defaultTargetPlatform != TargetPlatform.linux)) { - await tester.testDuration(audioSourceTestData.duration); - if (!audioSourceTestData.isLiveStream) { - await tester.testOnDuration( - audioSourceTestData.duration!, - timeout: timeout, - ); - } - } - if (!audioSourceTestData.isLiveStream) { - await tester.testPosition(Duration.zero); - } -} - -extension StreamWidgetTester on WidgetTester { - // Precision for position & duration: - // Android: millisecond - // Windows: millisecond - // Linux: millisecond - // Web: millisecond - // Darwin: millisecond - - Future stopStream() async { - final st = StackTrace.current.toString(); - - await scrollToAndTap(const Key('stop_button')); - await waitOneshot(const Key('toast-player-stopped-0'), stackTrace: st); - await pumpAndSettle(); - } - - Future testOnDuration( - Duration duration, { - Duration timeout = const Duration(seconds: 10), - }) async { - printWithTimeOnFailure('Test OnDuration: $duration'); - final st = StackTrace.current.toString(); - await waitFor( - () async => expectWidgetHasDuration( - const Key('onDurationText'), - matcher: (Duration? actual) => durationRangeMatcher( - actual, - duration, - deviation: const Duration(milliseconds: 500), - ), - ), - timeout: timeout, - stackTrace: st, - ); - } - - Future testOnPosition( - Duration position, { - Matcher Function(Duration) matcher = equals, - Duration timeout = const Duration(seconds: 10), - }) async { - printWithTimeOnFailure('Test OnPosition: $position'); - final st = StackTrace.current.toString(); - await waitFor( - () async => expectWidgetHasDuration( - const Key('onPositionText'), - matcher: matcher(position), - ), - pollInterval: const Duration(milliseconds: 250), - timeout: timeout, - stackTrace: st, - ); - } - - Future testOnPlayerState( - PlayerState playerState, { - Duration timeout = const Duration(seconds: 10), - }) async { - printWithTimeOnFailure('Test OnState: $playerState'); - final st = StackTrace.current.toString(); - await waitFor( - () async => expectWidgetHasText( - const Key('onStateText'), - matcher: equals(playerState.toString()), - ), - pollInterval: const Duration(milliseconds: 250), - timeout: timeout, - stackTrace: st, - ); - } -} diff --git a/packages/audioplayers/example/integration_test/app_test.dart b/packages/audioplayers/example/integration_test/app_test.dart index 217c94a48..830ec9bcd 100644 --- a/packages/audioplayers/example/integration_test/app_test.dart +++ b/packages/audioplayers/example/integration_test/app_test.dart @@ -2,43 +2,15 @@ import 'package:audioplayers_example/main.dart' as app; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'app/app_source_test_data.dart'; -import 'app/tabs/context_tab.dart'; -import 'app/tabs/controls_tab.dart'; -import 'app/tabs/logs_tab.dart'; -import 'app/tabs/source_tab.dart'; -import 'app/tabs/stream_tab.dart'; -import 'platform_features.dart'; - void main() { - final features = PlatformFeatures.instance(); - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('end-to-end test', () { - testWidgets('verify app is launched', (WidgetTester tester) async { + group('E2E Test', () { + testWidgets('verify app startup and title', (tester) async { app.main(); await tester.pumpAndSettle(); - expect( - find.text('Remote URL WAV 1'), - findsOneWidget, - ); - }); - }); - - group('test functionality of sources', () { - for (final audioSourceTestData in audioTestDataList) { - testWidgets('test source $audioSourceTestData', - (WidgetTester tester) async { - app.main(); - await tester.pumpAndSettle(); - await testSourcesTab(tester, audioSourceTestData, features); - await testControlsTab(tester, audioSourceTestData, features); - await testStreamsTab(tester, audioSourceTestData, features); - await testContextTab(tester, audioSourceTestData, features); - await testLogsTab(tester, audioSourceTestData, features); - }); - } + expect(find.text('AudioPlayers example'), findsOneWidget); + }); }); } diff --git a/packages/audioplayers/example/integration_test/lib/lib_source_test_data.dart b/packages/audioplayers/example/integration_test/lib/lib_source_test_data.dart deleted file mode 100644 index d63dc78c3..000000000 --- a/packages/audioplayers/example/integration_test/lib/lib_source_test_data.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'package:audioplayers/audioplayers.dart'; -import 'package:audioplayers_example/tabs/sources.dart'; -import 'package:http/http.dart'; - -import '../platform_features.dart'; -import '../source_test_data.dart'; - -/// Data of a library test source. -class LibSourceTestData extends SourceTestData { - Source source; - - LibSourceTestData({ - required this.source, - required super.duration, - super.isVBR, - }); - - @override - String toString() { - return 'LibSourceTestData(' - 'source: $source, ' - 'duration: $duration, ' - 'isVBR: $isVBR' - ')'; - } -} - -final _features = PlatformFeatures.instance(); - -final wavUrl1TestData = LibSourceTestData( - source: UrlSource(wavUrl1), - duration: const Duration(milliseconds: 451), -); - -final specialCharUrlTestData = LibSourceTestData( - source: UrlSource(wavUrl3), - duration: const Duration(milliseconds: 451), -); - -final mp3Url1TestData = LibSourceTestData( - source: UrlSource(mp3Url1), - duration: const Duration(minutes: 3, seconds: 30, milliseconds: 77), - isVBR: true, -); - -final m3u8UrlTestData = LibSourceTestData( - source: UrlSource(m3u8StreamUrl), - duration: null, -); - -final mpgaUrlTestData = LibSourceTestData( - source: UrlSource(mpgaStreamUrl), - duration: null, -); - -final wavAsset1TestData = LibSourceTestData( - source: AssetSource(wavAsset1), - duration: const Duration(milliseconds: 451), -); - -final wavAsset2TestData = LibSourceTestData( - source: AssetSource(wavAsset2), - duration: const Duration(seconds: 1, milliseconds: 068), -); - -final invalidAssetTestData = LibSourceTestData( - source: AssetSource(invalidAsset), - duration: null, -); - -final specialCharAssetTestData = LibSourceTestData( - source: AssetSource(specialCharAsset), - duration: const Duration(milliseconds: 451), -); - -final noExtensionAssetTestData = LibSourceTestData( - source: AssetSource(noExtensionAsset, mimeType: 'audio/wav'), - duration: const Duration(milliseconds: 451), -); - -final nonExistentUrlTestData = LibSourceTestData( - source: UrlSource('non_existent.txt'), - duration: null, -); - -final wavDataUriTestData = LibSourceTestData( - source: UrlSource(wavDataUri), - duration: const Duration(milliseconds: 451), -); - -final mp3DataUriTestData = LibSourceTestData( - source: UrlSource(mp3DataUri), - duration: const Duration(milliseconds: 444), -); - -Future mp3BytesTestData() async => LibSourceTestData( - source: BytesSource( - await readBytes(Uri.parse(mp3Url1)), - mimeType: 'audio/mpeg', - ), - duration: const Duration(minutes: 3, seconds: 30, milliseconds: 76), - ); - -// Some sources are commented which are considered redundant -Future> getAudioTestDataList() async { - return [ - if (_features.hasUrlSource) wavUrl1TestData, - /*if (_features.hasUrlSource) - LibSourceTestData( - source: UrlSource(wavUrl2), - duration: const Duration(seconds: 1, milliseconds: 068), - ),*/ - if (_features.hasUrlSource) mp3Url1TestData, - /*if (_features.hasUrlSource) - LibSourceTestData( - source: UrlSource(mp3Url2), - duration: const Duration(minutes: 1, seconds: 34, milliseconds: 119), - ),*/ - if (_features.hasUrlSource && _features.hasPlaylistSourceType) - m3u8UrlTestData, - if (_features.hasUrlSource) mpgaUrlTestData, - if (_features.hasDataUriSource) wavDataUriTestData, - // if (_features.hasDataUriSource) mp3DataUriTestData, - if (_features.hasAssetSource) wavAsset2TestData, - /*if (_features.hasAssetSource) - LibSourceTestData( - source: AssetSource(mp3Asset), - duration: const Duration(minutes: 1, seconds: 34, milliseconds: 119), - ),*/ - if (_features.hasBytesSource) await mp3BytesTestData(), - /*if (_features.hasBytesSource) - // Cache not working for web - LibSourceTestData( - source: BytesSource( - await AudioCache.instance.loadAsBytes(wavAsset2), - mimeType: 'audio/wav', - ), - duration: const Duration(seconds: 1, milliseconds: 068), - ),*/ - ]; -} diff --git a/packages/audioplayers/example/integration_test/lib/lib_test_utils.dart b/packages/audioplayers/example/integration_test/lib/lib_test_utils.dart deleted file mode 100644 index 552f080e7..000000000 --- a/packages/audioplayers/example/integration_test/lib/lib_test_utils.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; - -extension LibWidgetTester on WidgetTester { - Future pumpPlatform([ - Duration? duration, - EnginePhase phase = EnginePhase.sendSemanticsUpdate, - ]) async { - if (!kIsWeb && defaultTargetPlatform == TargetPlatform.linux) { - // FIXME(1556): Pump on Linux doesn't work with GStreamer bus callback - await Future.delayed(duration ?? Duration.zero); - } else { - await pump(duration, phase); - } - } - - /// See [pumpFrames]. - Future pumpGlobalFrames( - Duration maxDuration, [ - Duration interval = const Duration(milliseconds: 16, microseconds: 683), - ]) { - var elapsed = Duration.zero; - return TestAsyncUtils.guard(() async { - binding.scheduleFrame(); - while (elapsed < maxDuration) { - await binding.pump(interval); - elapsed += interval; - } - }); - } -} diff --git a/packages/audioplayers/example/integration_test/lib_test.dart b/packages/audioplayers/example/integration_test/lib_test.dart deleted file mode 100644 index 759e0b3a5..000000000 --- a/packages/audioplayers/example/integration_test/lib_test.dart +++ /dev/null @@ -1,403 +0,0 @@ -import 'package:audioplayers/audioplayers.dart'; -import 'package:audioplayers_example/tabs/sources.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import 'lib/lib_source_test_data.dart'; -import 'lib/lib_test_utils.dart'; -import 'platform_features.dart'; -import 'test_utils.dart'; - -void main() async { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - final features = PlatformFeatures.instance(); - final isAndroid = !kIsWeb && defaultTargetPlatform == TargetPlatform.android; - final audioTestDataList = await getAudioTestDataList(); - - testWidgets('test asset source with special char', - (WidgetTester tester) async { - final player = AudioPlayer(); - - await player.play(specialCharAssetTestData.source); - // Sources take some time to get initialized - await tester.pumpPlatform(const Duration(seconds: 8)); - await player.stop(); - - await player.dispose(); - }); - - testWidgets( - 'test device file source with special char', - (WidgetTester tester) async { - final player = AudioPlayer(); - - final path = await player.audioCache.loadPath(specialCharAsset); - expect(path, isNot(contains('%'))); // Ensure path is not URL encoded - await player.play(DeviceFileSource(path)); - // Sources take some time to get initialized - await tester.pumpPlatform(const Duration(seconds: 8)); - await player.stop(); - - await player.dispose(); - }, - skip: kIsWeb, - ); - - testWidgets('test url source with special char', (WidgetTester tester) async { - final player = AudioPlayer(); - - await player.play(specialCharUrlTestData.source); - // Sources take some time to get initialized - await tester.pumpPlatform(const Duration(seconds: 8)); - await player.stop(); - - await player.dispose(); - }); - - testWidgets( - 'test url source with no extension', - (WidgetTester tester) async { - final player = AudioPlayer(); - - await player.play(noExtensionAssetTestData.source); - // Sources take some time to get initialized - await tester.pumpPlatform(const Duration(seconds: 8)); - await player.stop(); - - await player.dispose(); - }, - ); - - testWidgets('data URI source', (WidgetTester tester) async { - final player = AudioPlayer(); - - await player.play(mp3DataUriTestData.source); - // Sources take some time to get initialized - await tester.pumpPlatform(const Duration(seconds: 8)); - await player.stop(); - - await player.dispose(); - }); - - testWidgets( - 'bytes array source', - (WidgetTester tester) async { - final player = AudioPlayer(); - - await player.play((await mp3BytesTestData()).source); - // Sources take some time to get initialized - await tester.pumpPlatform(const Duration(seconds: 8)); - await player.stop(); - - await player.dispose(); - }, - skip: !features.hasBytesSource, - ); - - group('AP events', () { - late AudioPlayer player; - - setUp(() async { - player = AudioPlayer( - playerId: 'somePlayerId', - ); - }); - - void testPositionUpdater( - LibSourceTestData td, { - bool useTimerPositionUpdater = false, - }) { - final positionUpdaterName = useTimerPositionUpdater - ? 'TimerPositionUpdater' - : 'FramePositionUpdater'; - testWidgets( - '#positionEvent with $positionUpdaterName: ${td.source}', - (tester) async { - if (useTimerPositionUpdater) { - player.positionUpdater = TimerPositionUpdater( - getPosition: player.getCurrentPosition, - interval: const Duration(milliseconds: 100), - ); - } - final futurePositions = player.onPositionChanged.toList(); - - await player.setReleaseMode(ReleaseMode.stop); - await player.setSource(td.source); - await player.resume(); - await tester.pumpGlobalFrames(const Duration(seconds: 5)); - - if (!td.isLiveStream && td.duration! < const Duration(seconds: 2)) { - expect(player.state, PlayerState.completed); - } else { - if (td.isLiveStream || td.duration! > const Duration(seconds: 10)) { - expect(player.state, PlayerState.playing); - } else { - // Don't know for sure, if has yet completed or is still playing - } - await player.stop(); - expect(player.state, PlayerState.stopped); - } - await player.dispose(); - final positions = await futurePositions; - printOnFailure('Positions: $positions'); - expect(positions, isNot(contains(null))); - expect(positions, contains(greaterThan(Duration.zero))); - if (td.isLiveStream) { - // TODO(gustl22): Live streams may have zero or null as initial - // position. This should be consistent across all platforms. - } else { - expect(positions.first, Duration.zero); - expect(positions.last, Duration.zero); - } - }, - skip: - // FIXME(gustl22): [FLAKY] macos 13 fails on live streams. - (isMacOS && td.isLiveStream) || - // FIXME(gustl22): Android provides no position for samples - // shorter than 0.5 seconds. - (isAndroid && - !td.isLiveStream && - td.duration! < const Duration(seconds: 1)), - ); - } - - /// Test at least one source with [TimerPositionUpdater]. - testPositionUpdater(mp3Url1TestData, useTimerPositionUpdater: true); - - for (final td in audioTestDataList) { - testPositionUpdater(td); - } - }); - - group('play multiple sources', () { - testWidgets( - 'play multiple sources simultaneously', - (WidgetTester tester) async { - final players = - List.generate(audioTestDataList.length, (_) => AudioPlayer()); - - // Start all players simultaneously - final iterator = List.generate(audioTestDataList.length, (i) => i); - await Future.wait( - iterator.map((i) => players[i].play(audioTestDataList[i].source)), - ); - // Sources take some time to get initialized - await tester.pumpPlatform(const Duration(seconds: 8)); - for (var i = 0; i < audioTestDataList.length; i++) { - final td = audioTestDataList[i]; - if (td.isLiveStream || td.duration! > const Duration(seconds: 10)) { - final position = await players[i].getCurrentPosition(); - printWithTimeOnFailure('Test position: $td'); - expect(position, greaterThan(Duration.zero)); - } - await players[i].stop(); - } - await Future.wait(players.map((p) => p.dispose())); - }, - // FIXME: Causes media error on Android (see #1333, #1353) - // Unexpected platform error: MediaPlayer error with - // what:MEDIA_ERROR_UNKNOWN {what:1} extra:MEDIA_ERROR_SYSTEM - skip: isAndroid, - ); - - testWidgets('play multiple sources consecutively', - (WidgetTester tester) async { - final player = AudioPlayer(); - - for (final td in audioTestDataList) { - await player.play(td.source); - // Sources take some time to get initialized - await tester.pumpPlatform(const Duration(seconds: 8)); - if (td.isLiveStream || td.duration! > const Duration(seconds: 10)) { - final position = await player.getCurrentPosition(); - printWithTimeOnFailure('Test position: $td'); - expect(position, greaterThan(Duration.zero)); - } - await player.stop(); - } - await player.dispose(); - }); - }); - - group('Audio Context', () { - /// Android and iOS only: Play the same sound twice with a different audio - /// context each. This test can be executed on a device, with either - /// "Silent", "Vibrate" or "Ring" mode. In "Silent" or "Vibrate" mode - /// the second sound should not be audible. - testWidgets( - 'test changing AudioContextConfigs', - (WidgetTester tester) async { - final player = AudioPlayer(); - await player.setReleaseMode(ReleaseMode.stop); - - final td = wavUrl1TestData; - - var audioContext = AudioContextConfig( - //ignore: avoid_redundant_argument_values - route: AudioContextConfigRoute.system, - //ignore: avoid_redundant_argument_values - respectSilence: false, - ).build(); - await AudioPlayer.global.setAudioContext(audioContext); - await player.setAudioContext(audioContext); - - await player.play(td.source); - await tester.pumpPlatform( - (td.duration ?? Duration.zero) + const Duration(seconds: 8), - ); - expect(player.state, PlayerState.completed); - - audioContext = AudioContextConfig( - //ignore: avoid_redundant_argument_values - route: AudioContextConfigRoute.system, - respectSilence: true, - ).build(); - await AudioPlayer.global.setAudioContext(audioContext); - await player.setAudioContext(audioContext); - - await player.resume(); - await tester.pumpPlatform( - (td.duration ?? Duration.zero) + const Duration(seconds: 8), - ); - expect(player.state, PlayerState.completed); - await player.dispose(); - }, - skip: !features.hasRespectSilence, - ); - - testWidgets( - 'Set global AudioContextConfig on unsupported platforms', - (WidgetTester tester) async { - final audioContext = AudioContextConfig().build(); - final globalLogFuture = AudioPlayer.global.onLog.first; - await AudioPlayer.global.setAudioContext(audioContext); - - expect( - await globalLogFuture, - contains('Setting AudioContext is not supported'), - ); - - final player = AudioPlayer(); - final logFuture = player.onLog.first; - await player.setAudioContext(audioContext); - expect( - await logFuture, - contains('Setting AudioContext is not supported'), - ); - - await player.dispose(); - }, - skip: features.hasRespectSilence, - ); - - /// Android and iOS only: Play the same sound twice with a different audio - /// context each. This test can be executed on a device, with either - /// "Silent", "Vibrate" or "Ring" mode. In "Silent" or "Vibrate" mode - /// the second sound should not be audible. - testWidgets( - 'test changing AudioContextConfigs in LOW_LATENCY mode', - (WidgetTester tester) async { - final player = AudioPlayer(); - await player.setReleaseMode(ReleaseMode.stop); - player.setPlayerMode(PlayerMode.lowLatency); - - final td = wavUrl1TestData; - - var audioContext = AudioContextConfig( - //ignore: avoid_redundant_argument_values - route: AudioContextConfigRoute.system, - //ignore: avoid_redundant_argument_values - respectSilence: false, - ).build(); - await AudioPlayer.global.setAudioContext(audioContext); - await player.setAudioContext(audioContext); - - await player.setSource(td.source); - await player.resume(); - await tester.pumpPlatform( - (td.duration ?? Duration.zero) + const Duration(seconds: 8), - ); - expect(player.state, PlayerState.playing); - await player.stop(); - expect(player.state, PlayerState.stopped); - - audioContext = AudioContextConfig( - //ignore: avoid_redundant_argument_values - route: AudioContextConfigRoute.system, - respectSilence: true, - ).build(); - await AudioPlayer.global.setAudioContext(audioContext); - await player.setAudioContext(audioContext); - - await player.resume(); - await tester.pumpPlatform( - (td.duration ?? Duration.zero) + const Duration(seconds: 8), - ); - expect(player.state, PlayerState.playing); - await player.stop(); - expect(player.state, PlayerState.stopped); - await player.dispose(); - }, - skip: !features.hasRespectSilence || !features.hasLowLatency, - ); - }); - - testWidgets('Race condition on play and pause (#1687)', - (WidgetTester tester) async { - final player = AudioPlayer(); - - final futurePlay = player.play(mp3Url1TestData.source); - - // Player is still in `stopped` state as it isn't playing yet. - expect(player.state, PlayerState.stopped); - expect(player.desiredState, PlayerState.playing); - - // Execute `pause` before `play` has finished. - final futurePause = player.pause(); - expect(player.desiredState, PlayerState.paused); - - await futurePlay; - await futurePause; - - expect(player.state, PlayerState.paused); - - await player.dispose(); - }); - - group( - 'Android only:', - () { - /// The test is auditory only! - /// It will succeed even if the wrong source is played. - testWidgets('Released wrong source on LOW_LATENCY (#1672)', - (WidgetTester tester) async { - var player = AudioPlayer() - ..setPlayerMode(PlayerMode.lowLatency) - ..setReleaseMode(ReleaseMode.stop); - - await player.play(wavAsset1TestData.source); - await tester.pumpPlatform(const Duration(seconds: 1)); - await player.stop(); - - await player.play(wavAsset2TestData.source); - await tester.pumpPlatform(const Duration(seconds: 1)); - await player.stop(); - - player = AudioPlayer() - ..setPlayerMode(PlayerMode.lowLatency) - ..setReleaseMode(ReleaseMode.stop); - - // This should play the new source, not the old one: - await player.play(wavAsset1TestData.source); - await tester.pumpPlatform(const Duration(seconds: 1)); - await player.stop(); - - await player.play(wavAsset2TestData.source); - await tester.pumpPlatform(const Duration(seconds: 1)); - await player.stop(); - }); - }, - skip: !features.hasLowLatency, - ); -} diff --git a/packages/audioplayers/example/integration_test/platform_features.dart b/packages/audioplayers/example/integration_test/platform_features.dart deleted file mode 100644 index ad9b5bb01..000000000 --- a/packages/audioplayers/example/integration_test/platform_features.dart +++ /dev/null @@ -1,158 +0,0 @@ -import 'package:flutter/foundation.dart'; - -const testFeatureBytesSource = bool.fromEnvironment( - 'TEST_FEATURE_BYTES_SOURCE', - defaultValue: true, -); - -const testFeaturePlaybackRate = bool.fromEnvironment( - 'TEST_FEATURE_PLAYBACK_RATE', - defaultValue: true, -); - -const testFeatureLowLatency = bool.fromEnvironment( - 'TEST_FEATURE_LOW_LATENCY', - defaultValue: true, -); - -/// Specify supported features for a platform. -class PlatformFeatures { - static const webPlatformFeatures = PlatformFeatures( - hasPlaylistSourceType: false, - hasLowLatency: false, - hasForceSpeaker: false, - hasDuckAudio: false, - hasRespectSilence: false, - hasStayAwake: false, - hasRecordingActive: false, - hasPlayingRoute: false, - hasErrorEvent: false, - ); - - static const androidPlatformFeatures = PlatformFeatures( - hasRecordingActive: false, - // ignore: avoid_redundant_argument_values - hasBytesSource: testFeatureBytesSource, - // ignore: avoid_redundant_argument_values - hasPlaybackRate: testFeaturePlaybackRate, - // ignore: avoid_redundant_argument_values - hasLowLatency: testFeatureLowLatency, - ); - - static const iosPlatformFeatures = PlatformFeatures( - hasDataUriSource: false, - hasBytesSource: false, - hasPlaylistSourceType: false, - hasLowLatency: false, - hasBalance: false, - ); - - static const macPlatformFeatures = PlatformFeatures( - hasDataUriSource: false, - hasBytesSource: false, - hasPlaylistSourceType: false, - hasLowLatency: false, - hasForceSpeaker: false, - hasDuckAudio: false, - hasRespectSilence: false, - hasStayAwake: false, - hasRecordingActive: false, - hasPlayingRoute: false, - hasBalance: false, - ); - - static const linuxPlatformFeatures = PlatformFeatures( - hasDataUriSource: false, - hasBytesSource: false, - hasLowLatency: false, - // MP3 duration is estimated: https://bugzilla.gnome.org/show_bug.cgi?id=726144 - // Use GstDiscoverer to get duration before playing: https://gstreamer.freedesktop.org/documentation/pbutils/gstdiscoverer.html?gi-language=c - hasMp3Duration: false, - hasForceSpeaker: false, - hasDuckAudio: false, - hasRespectSilence: false, - hasStayAwake: false, - hasRecordingActive: false, - hasPlayingRoute: false, - ); - - static const windowsPlatformFeatures = PlatformFeatures( - hasDataUriSource: false, - hasPlaylistSourceType: false, - hasLowLatency: false, - hasForceSpeaker: false, - hasDuckAudio: false, - hasRespectSilence: false, - hasStayAwake: false, - hasRecordingActive: false, - hasPlayingRoute: false, - ); - - final bool hasUrlSource; - final bool hasDataUriSource; - final bool hasAssetSource; - final bool hasBytesSource; - - final bool hasPlaylistSourceType; - - final bool hasLowLatency; - final bool hasReleaseModeRelease; - final bool hasReleaseModeLoop; - final bool hasVolume; - final bool hasBalance; - final bool hasSeek; - final bool hasMp3Duration; - - final bool hasPlaybackRate; - final bool hasForceSpeaker; // Not yet tested - final bool hasDuckAudio; // Not yet tested - final bool hasRespectSilence; - final bool hasStayAwake; // Not yet tested - final bool hasRecordingActive; // Not yet tested - final bool hasPlayingRoute; // Not yet tested - - final bool hasDurationEvent; - final bool hasPlayerStateEvent; - final bool hasErrorEvent; // Not yet tested - - const PlatformFeatures({ - this.hasUrlSource = true, - this.hasDataUriSource = true, - this.hasAssetSource = true, - this.hasBytesSource = true, - this.hasPlaylistSourceType = true, - this.hasLowLatency = true, - this.hasReleaseModeRelease = true, - this.hasReleaseModeLoop = true, - this.hasMp3Duration = true, - this.hasVolume = true, - this.hasBalance = true, - this.hasSeek = true, - this.hasPlaybackRate = true, - this.hasForceSpeaker = true, - this.hasDuckAudio = true, - this.hasRespectSilence = true, - this.hasStayAwake = true, - this.hasRecordingActive = true, - this.hasPlayingRoute = true, - this.hasDurationEvent = true, - this.hasPlayerStateEvent = true, - this.hasErrorEvent = true, - }); - - factory PlatformFeatures.instance() { - return kIsWeb - ? webPlatformFeatures - : defaultTargetPlatform == TargetPlatform.android - ? androidPlatformFeatures - : defaultTargetPlatform == TargetPlatform.iOS - ? iosPlatformFeatures - : defaultTargetPlatform == TargetPlatform.macOS - ? macPlatformFeatures - : defaultTargetPlatform == TargetPlatform.linux - ? linuxPlatformFeatures - : defaultTargetPlatform == TargetPlatform.windows - ? windowsPlatformFeatures - : const PlatformFeatures(); - } -} diff --git a/packages/audioplayers/example/integration_test/platform_test.dart b/packages/audioplayers/example/integration_test/platform_test.dart deleted file mode 100644 index 43bc98607..000000000 --- a/packages/audioplayers/example/integration_test/platform_test.dart +++ /dev/null @@ -1,552 +0,0 @@ -import 'dart:async'; - -import 'package:audioplayers/audioplayers.dart'; -import 'package:audioplayers_platform_interface/audioplayers_platform_interface.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import 'lib/lib_source_test_data.dart'; -import 'platform_features.dart'; -import 'source_test_data.dart'; -import 'test_utils.dart'; - -const _defaultTimeout = Duration(seconds: 30); - -final isLinux = !kIsWeb && defaultTargetPlatform == TargetPlatform.linux; -final isAndroid = !kIsWeb && defaultTargetPlatform == TargetPlatform.android; - -bool canDetermineDuration(SourceTestData td) { - // TODO(gustl22): cannot determine duration for VBR on Linux - // FIXME(gustl22): duration event is not emitted for short duration - // WAV on Linux (only platform tests, may be a race condition). - if (td.duration == null) { - return true; - } - if (isLinux) { - return !(td.isVBR || td.duration! < const Duration(seconds: 5)); - } - return true; -} - -void main() async { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - final features = PlatformFeatures.instance(); - final audioTestDataList = await getAudioTestDataList(); - - group('Platform method channel', () { - late AudioplayersPlatformInterface platform; - late String playerId; - - setUp(() async { - platform = AudioplayersPlatformInterface.instance; - playerId = 'somePlayerId'; - await platform.create(playerId); - }); - - tearDown(() async { - await platform.dispose(playerId); - }); - - testWidgets( - 'Throw PlatformException, when loading invalid file', - (tester) async { - // Throws PlatformException instead of returning prepared event. - await tester.expectSettingSourceFailure( - future: tester.prepareSource( - playerId: playerId, - platform: platform, - testData: invalidAssetTestData, - ), - ); - - if (isLinux) { - // Linux throws a second failure event for invalid files. - // If not caught, it would be randomly thrown in the following tests. - final nextEvent = platform.getEventStream(playerId).first; - await tester.expectSettingSourceFailure(future: nextEvent); - } - }, - ); - - testWidgets( - 'Throw PlatformException, when loading non existent file', - (tester) async { - // Throws PlatformException instead of returning prepared event. - await tester.expectSettingSourceFailure( - future: tester.prepareSource( - playerId: playerId, - platform: platform, - testData: nonExistentUrlTestData, - ), - ); - }, - // FIXME(Gustl22): for some reason, the error propagated back from the - // Android MediaPlayer is only triggered, when the timeout has reached, - // although the error is emitted immediately. - // Further, the other future is not fulfilled and then mysteriously - // failing in later tests. - skip: isAndroid, - ); - - testWidgets('#create and #dispose', (tester) async { - await platform.dispose(playerId); - - try { - // Call method after player has been released should throw a - // PlatformException - await platform.stop(playerId); - fail('PlatformException not thrown'); - } on PlatformException catch (e) { - expect( - e.message, - 'Player has not yet been created or has already been disposed.', - ); - } - - // Create player again, so it can be disposed in tearDown - await platform.create(playerId); - }); - - for (final td in audioTestDataList) { - testWidgets( - '#setSource #getPosition and #getDuration ${td.source}', - (tester) async { - await tester.prepareSource( - playerId: playerId, - platform: platform, - testData: td, - ); - if (!td.isLiveStream) { - // Live stream position is not aligned yet. - expect(await platform.getCurrentPosition(playerId), 0); - } - final durationMs = await platform.getDuration(playerId); - expect( - durationMs != null ? Duration(milliseconds: durationMs) : null, - // TODO(gustl22): once duration is always null for streams, - // then can remove fallback for Duration.zero - (Duration? actual) => durationRangeMatcher( - actual ?? Duration.zero, - td.duration ?? Duration.zero, - deviation: Duration(milliseconds: td.isVBR ? 100 : 1), - ), - ); - }, - // FIXME(gustl22): determines wrong initial position for m3u8 on Linux - skip: !canDetermineDuration(td) || - isLinux && td.source == m3u8UrlTestData.source, - ); - } - - if (features.hasVolume) { - for (final td in audioTestDataList) { - testWidgets('#volume ${td.source}', (tester) async { - await tester.prepareSource( - playerId: playerId, - platform: platform, - testData: td, - ); - for (final volume in [0.0, 0.5, 1.0]) { - await platform.setVolume(playerId, volume); - await platform.resume(playerId); - await tester.pump(const Duration(seconds: 1)); - await platform.stop(playerId); - } - // May check native volume here - }); - } - } - - if (features.hasBalance) { - for (final td in audioTestDataList) { - testWidgets('#balance ${td.source}', (tester) async { - await tester.prepareSource( - playerId: playerId, - platform: platform, - testData: td, - ); - for (final balance in [-1.0, 0.0, 1.0]) { - await platform.setBalance(playerId, balance); - await platform.resume(playerId); - await tester.pump(const Duration(seconds: 1)); - await platform.stop(playerId); - } - // May check native balance here - }); - } - } - - for (final td in audioTestDataList) { - if (features.hasPlaybackRate && !td.isLiveStream) { - testWidgets('#playbackRate ${td.source}', (tester) async { - await tester.prepareSource( - playerId: playerId, - platform: platform, - testData: td, - ); - for (final playbackRate in [0.5, 1.0, 2.0]) { - await platform.setPlaybackRate(playerId, playbackRate); - await platform.resume(playerId); - await tester.pump(const Duration(seconds: 1)); - await platform.stop(playerId); - } - // May check native playback rate here - }); - } - } - - testWidgets('Avoid resume on setting playbackRate (#468)', (tester) async { - await tester.prepareSource( - playerId: playerId, - platform: platform, - testData: mp3Url1TestData, - ); - await platform.setPlaybackRate(playerId, 2.0); - await tester.pumpAndSettle(const Duration(seconds: 2)); - expect(await platform.getCurrentPosition(playerId), 0); - }); - - for (final td in audioTestDataList) { - if (features.hasSeek && !td.isLiveStream) { - testWidgets('#seek with millisecond precision ${td.source}', - (tester) async { - await tester.prepareSource( - playerId: playerId, - platform: platform, - testData: td, - ); - - final eventStream = platform.getEventStream(playerId); - final seekCompleter = Completer(); - final onSeekSub = eventStream - .where((event) => event.eventType == AudioEventType.seekComplete) - .listen( - (_) => seekCompleter.complete(), - onError: seekCompleter.completeError, - ); - await platform.seek(playerId, const Duration(milliseconds: 22)); - await seekCompleter.future.timeout(_defaultTimeout); - await onSeekSub.cancel(); - final positionMs = await platform.getCurrentPosition(playerId); - expect( - positionMs != null ? Duration(milliseconds: positionMs) : null, - (Duration? actual) => durationRangeMatcher( - actual, - const Duration(milliseconds: 22), - deviation: const Duration(milliseconds: 1), - ), - ); - }); - } - } - - for (final td in audioTestDataList) { - if (features.hasReleaseModeLoop && - !td.isLiveStream && - td.duration! < const Duration(seconds: 2)) { - testWidgets('#ReleaseMode.loop ${td.source}', (tester) async { - await tester.prepareSource( - playerId: playerId, - platform: platform, - testData: td, - ); - await platform.setReleaseMode(playerId, ReleaseMode.loop); - await platform.resume(playerId); - await tester.pump(const Duration(seconds: 3)); - await platform.stop(playerId); - - // May check number of loops here - }); - } - } - - for (final td in audioTestDataList) { - if (features.hasReleaseModeRelease && !td.isLiveStream) { - testWidgets('#ReleaseMode.release ${td.source}', (tester) async { - await tester.prepareSource( - playerId: playerId, - platform: platform, - testData: td, - ); - await platform.setReleaseMode(playerId, ReleaseMode.release); - await platform.resume(playerId); - if (td.duration! < const Duration(seconds: 2)) { - await tester.pumpAndSettle(const Duration(seconds: 3)); - // No need to call stop, as it should be released by now - } else { - await tester.pumpAndSettle(const Duration(seconds: 1)); - await platform.stop(playerId); - } - // TODO(Gustl22): test if source was released - expect(await platform.getDuration(playerId), null); - expect(await platform.getCurrentPosition(playerId), null); - }); - } - } - - for (final td in audioTestDataList) { - testWidgets('#release ${td.source}', (tester) async { - await tester.prepareSource( - playerId: playerId, - platform: platform, - testData: td, - ); - await tester.pump(const Duration(seconds: 1)); - await platform.release(playerId); - // TODO(Gustl22): test if source was released - // Check if position & duration is zero after play & release - expect(await platform.getDuration(playerId), null); - expect(await platform.getCurrentPosition(playerId), null); - }); - } - - testWidgets('Set same source twice (#1520)', (tester) async { - for (var i = 0; i < 2; i++) { - await tester.prepareSource( - playerId: playerId, - platform: platform, - testData: wavUrl1TestData, - // We don't expect the duration event is emitted again, - // if the same source is set twice - waitForDurationEvent: i == 0, - ); - } - }); - }); - - group('Platform event channel', () { - late AudioplayersPlatformInterface platform; - late String playerId; - - setUp(() async { - platform = AudioplayersPlatformInterface.instance; - playerId = 'somePlayerId'; - await platform.create(playerId); - }); - - tearDown(() async { - await platform.dispose(playerId); - }); - - for (final td in audioTestDataList) { - if (features.hasDurationEvent && !td.isLiveStream) { - testWidgets( - '#durationEvent ${td.source}', - (tester) async { - // Wait for duration before event is emitted. - final durationFuture = tester - .getDurationFromEvent( - playerId: playerId, - platform: platform, - ) - .timeout(_defaultTimeout); - - await tester.prepareSource( - playerId: playerId, - platform: platform, - testData: td, - waitForDurationEvent: false, - ); - - expect( - await durationFuture, - (Duration? actual) => durationRangeMatcher( - actual, - td.duration, - deviation: - Duration(milliseconds: td.isVBR || isWindows ? 100 : 1), - ), - ); - }, - skip: !canDetermineDuration(td), - ); - } - } - - for (final td in audioTestDataList) { - if (!td.isLiveStream && td.duration! < const Duration(seconds: 2)) { - testWidgets('#completeEvent ${td.source}', (tester) async { - await tester.prepareSource( - playerId: playerId, - platform: platform, - testData: td, - ); - - final eventStream = platform.getEventStream(playerId); - final completeFuture = eventStream.firstWhere( - (event) => event.eventType == AudioEventType.complete, - ); - - await platform.resume(playerId); - await tester.pumpAndSettle(const Duration(seconds: 3)); - await completeFuture.timeout(_defaultTimeout); - }); - } - } - - testWidgets('Listen and cancel twice', (tester) async { - final eventStream = platform.getEventStream(playerId); - for (var i = 0; i < 2; i++) { - final eventSub = eventStream.listen(null); - await eventSub.cancel(); - } - }); - - testWidgets('Emit platform log', (tester) async { - final logFuture = platform - .getEventStream(playerId) - .firstWhere((event) => event.eventType == AudioEventType.log) - .then((event) => event.logMessage); - - await platform.emitLog(playerId, 'SomeLog'); - - expect(await logFuture, 'SomeLog'); - }); - - testWidgets('Emit global platform log', (tester) async { - final global = GlobalAudioplayersPlatformInterface.instance; - final logCompleter = Completer(); - - /* final eventStreamSub = */ - global - .getGlobalEventStream() - .where((event) => event.eventType == GlobalAudioEventType.log) - .map((event) => event.logMessage) - .listen(logCompleter.complete, onError: logCompleter.completeError); - - await global.emitGlobalLog('SomeGlobalLog'); - - final log = await logCompleter.future; - expect(log, 'SomeGlobalLog'); - // FIXME: cancelling the global event stream leads to - // MissingPluginException on Android, if dispose app afterwards - // await eventStreamSub.cancel(); - }); - - testWidgets('Emit platform error', (tester) async { - final errorCompleter = Completer(); - final eventStreamSub = platform - .getEventStream(playerId) - .listen((_) {}, onError: errorCompleter.complete); - - await platform.emitError( - playerId, - 'SomeErrorCode', - 'SomeErrorMessage', - ); - - final exception = await errorCompleter.future; - expect(exception, isInstanceOf()); - final platformException = exception as PlatformException; - expect(platformException.code, 'SomeErrorCode'); - expect(platformException.message, 'SomeErrorMessage'); - await eventStreamSub.cancel(); - }); - - testWidgets('Emit global platform error', (tester) async { - final global = GlobalAudioplayersPlatformInterface.instance; - final errorCompleter = Completer(); - - /* final eventStreamSub = */ - global - .getGlobalEventStream() - .listen((_) {}, onError: errorCompleter.complete); - - await global.emitGlobalError( - 'SomeGlobalErrorCode', - 'SomeGlobalErrorMessage', - ); - final exception = await errorCompleter.future; - expect(exception, isInstanceOf()); - final platformException = exception as PlatformException; - expect(platformException.code, 'SomeGlobalErrorCode'); - expect(platformException.message, 'SomeGlobalErrorMessage'); - // FIXME: cancelling the global event stream leads to - // MissingPluginException on Android, if dispose app afterwards - // await eventStreamSub.cancel(); - }); - }); -} - -extension on WidgetTester { - Future prepareSource({ - required String playerId, - required AudioplayersPlatformInterface platform, - required LibSourceTestData testData, - bool waitForDurationEvent = true, - }) async { - final Future? durationFuture; - - if (waitForDurationEvent && - testData.duration != null && - canDetermineDuration(testData)) { - // Need to wait for the duration event, - // otherwise it gets fired/received after the test has ended, - // and therefore then ends up being received in the next test. - durationFuture = getDurationFromEvent( - playerId: playerId, - platform: platform, - ); - } else { - durationFuture = null; - } - - final eventStream = platform.getEventStream(playerId); - final preparedFuture = eventStream - .firstWhere( - (event) => - event.eventType == AudioEventType.prepared && - (event.isPrepared ?? false), - ) - .timeout(_defaultTimeout); - - Future setSource(Source source) async { - if (source is UrlSource) { - return platform.setSourceUrl(playerId, source.url); - } else if (source is AssetSource) { - final cachePath = await AudioCache.instance.loadPath(source.path); - return platform.setSourceUrl(playerId, cachePath, isLocal: true); - } else if (source is BytesSource) { - return platform.setSourceBytes(playerId, source.bytes); - } else { - throw 'Unknown source type: ${source.runtimeType}'; - } - } - - // Need to await the setting the source to propagate immediate errors. - final setSourceFuture = setSource(testData.source); - - // Wait simultaneously to ensure all errors are propagated through the same - // future. - await Future.wait([setSourceFuture, preparedFuture]); - if (durationFuture != null) { - await durationFuture; - } - } - - Future getDurationFromEvent({ - required String playerId, - required AudioplayersPlatformInterface platform, - }) async { - final eventStream = platform.getEventStream(playerId); - final durationFuture = eventStream - .firstWhere( - (event) => event.eventType == AudioEventType.duration, - ) - .then((event) => event.duration); - return durationFuture.timeout(_defaultTimeout); - } - - Future expectSettingSourceFailure({ - required Future future, - }) async { - try { - await future; - fail('PlatformException not thrown'); - } on PlatformException catch (e) { - expect(e.message, startsWith('Failed to set source.')); - } - } -} diff --git a/packages/audioplayers/example/integration_test/source_test_data.dart b/packages/audioplayers/example/integration_test/source_test_data.dart deleted file mode 100644 index eb49a8606..000000000 --- a/packages/audioplayers/example/integration_test/source_test_data.dart +++ /dev/null @@ -1,22 +0,0 @@ -/// Data of a ui test source. -abstract class SourceTestData { - Duration? duration; - - bool get isLiveStream => duration == null; - - /// Whether this source has variable bitrate - bool isVBR; - - SourceTestData({ - required this.duration, - this.isVBR = false, - }); - - @override - String toString() { - return 'SourceTestData(' - 'duration: $duration, ' - 'isVBR: $isVBR' - ')'; - } -} diff --git a/packages/audioplayers/example/integration_test/test_utils.dart b/packages/audioplayers/example/integration_test/test_utils.dart deleted file mode 100644 index 2e05c5ea2..000000000 --- a/packages/audioplayers/example/integration_test/test_utils.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -void printWithTimeOnFailure(String message) { - printOnFailure('${DateTime.now()}: $message'); -} - -bool durationRangeMatcher( - Duration? actual, - Duration? expected, { - Duration deviation = const Duration(seconds: 1), -}) { - if (actual == null && expected == null) { - return true; - } - if (actual == null || expected == null) { - return false; - } - return actual >= (expected - deviation) && actual <= (expected + deviation); -} diff --git a/packages/audioplayers/example/lib/tabs/sources.dart b/packages/audioplayers/example/lib/tabs/sources.dart index 7d2ab8e80..6ec209574 100644 --- a/packages/audioplayers/example/lib/tabs/sources.dart +++ b/packages/audioplayers/example/lib/tabs/sources.dart @@ -58,20 +58,36 @@ class _SourcesTabState extends State final List sourceWidgets = []; Future _setSource(Source source) async { - await player.setSource(source); - toast( - 'Completed setting source.', - textKey: const Key('toast-set-source'), - ); + try { + await player.setSource(source); + toast( + 'Completed setting source.', + textKey: const Key('toast-set-source'), + ); + } on Exception catch (e, stackTrace) { + AudioLogger.error(e, stackTrace); + toast( + 'Error setting source: $e', + textKey: const Key('toast-error-set-source'), + ); + } } Future _play(Source source) async { - await player.stop(); - await player.play(source); - toast( - 'Set and playing source.', - textKey: const Key('toast-set-play'), - ); + try { + await player.stop(); + await player.play(source); + toast( + 'Set and playing source.', + textKey: const Key('toast-set-play'), + ); + } on Exception catch (e, stackTrace) { + AudioLogger.error(e, stackTrace); + toast( + 'Error playing source: $e', + textKey: const Key('toast-error-play'), + ); + } } Future _removeSourceWidget(Widget sourceWidget) async { @@ -105,8 +121,16 @@ class _SourcesTabState extends State required String asset, String? mimeType, }) async { - final bytes = await AudioCache.instance.loadAsBytes(asset); - await fun(BytesSource(bytes, mimeType: mimeType)); + try { + final bytes = await AudioCache.instance.loadAsBytes(asset); + await fun(BytesSource(bytes, mimeType: mimeType)); + } on Exception catch (e, stackTrace) { + AudioLogger.error(e, stackTrace); + toast( + 'Error loading bytes from asset: $e', + textKey: const Key('toast-error-bytes-asset'), + ); + } } Future _setSourceBytesRemote( @@ -114,8 +138,16 @@ class _SourcesTabState extends State required String url, String? mimeType, }) async { - final bytes = await http.readBytes(Uri.parse(url)); - await fun(BytesSource(bytes, mimeType: mimeType)); + try { + final bytes = await http.readBytes(Uri.parse(url)); + await fun(BytesSource(bytes, mimeType: mimeType)); + } on Exception catch (e, stackTrace) { + AudioLogger.error(e, stackTrace); + toast( + 'Error loading bytes from URL: $e', + textKey: const Key('toast-error-bytes-remote'), + ); + } } @override @@ -269,8 +301,8 @@ class _SourcesTabState extends State } class _SourceTile extends StatelessWidget { - final void Function() setSource; - final void Function() play; + final Future Function() setSource; + final Future Function() play; final void Function(Widget sourceWidget) removeSource; final String title; final String? subtitle; @@ -300,14 +332,26 @@ class _SourceTile extends StatelessWidget { IconButton( tooltip: 'Set Source', key: setSourceKey, - onPressed: setSource, + onPressed: () async { + try { + await setSource(); + } on Exception catch (e, stackTrace) { + AudioLogger.error(e, stackTrace); + } + }, icon: const Icon(Icons.upload_file), color: buttonColor ?? Theme.of(context).primaryColor, ), IconButton( key: playKey, tooltip: 'Play', - onPressed: play, + onPressed: () async { + try { + await play(); + } on Exception catch (e, stackTrace) { + AudioLogger.error(e, stackTrace); + } + }, icon: const Icon(Icons.play_arrow), color: buttonColor ?? Theme.of(context).primaryColor, ), diff --git a/packages/audioplayers/example/pubspec.yaml b/packages/audioplayers/example/pubspec.yaml index 118366b68..c9427807a 100644 --- a/packages/audioplayers/example/pubspec.yaml +++ b/packages/audioplayers/example/pubspec.yaml @@ -6,12 +6,13 @@ publish_to: none dependencies: audioplayers: ^6.5.1 collection: ^1.16.0 - file_picker: ^8.0.3 + file_picker: ^10.3.8 flutter: sdk: flutter http: ^1.0.0 + integration_test: + sdk: flutter path_provider: ^2.0.12 - provider: ^6.0.5 dev_dependencies: # Integration tests for audioplayers_platform_interface are handled @@ -20,8 +21,6 @@ dev_dependencies: flame_lint: ^1.4.1 flutter_test: sdk: flutter - integration_test: - sdk: flutter flutter: uses-material-design: true diff --git a/packages/audioplayers/example/test_driver/integration_test.dart b/packages/audioplayers/example/test_driver/integration_test.dart deleted file mode 100644 index b38629cca..000000000 --- a/packages/audioplayers/example/test_driver/integration_test.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); diff --git a/packages/audioplayers/lib/src/audioplayer.dart b/packages/audioplayers/lib/src/audioplayer.dart index 94f9f8403..aaf64a9eb 100644 --- a/packages/audioplayers/lib/src/audioplayer.dart +++ b/packages/audioplayers/lib/src/audioplayer.dart @@ -179,9 +179,15 @@ class AudioPlayer { await _platform.create(playerId); // Assign the event stream, now that the platform registered this player. _eventStreamSubscription = _platform.getEventStream(playerId).listen( - _eventStreamController.add, - onError: _eventStreamController.addError, + _eventStreamController.add, + onError: (Object e, [StackTrace? stackTrace]) { + // Log error but DON'T propagate to prevent unhandled exception + AudioLogger.error( + AudioPlayerException(this, cause: e), + stackTrace, ); + }, + ); creatingCompleter.complete(); } on Exception catch (e, stackTrace) { creatingCompleter.completeError(e, stackTrace); @@ -358,18 +364,30 @@ class AudioPlayer { Future _completePrepared(Future Function() setSource) async { await creatingCompleter.future; - final preparedFuture = _onPrepared - .firstWhere((isPrepared) => isPrepared) - .timeout(AudioPlayer.preparationTimeout); - // Need to await the setting the source to propagate immediate errors. - final setSourceFuture = setSource(); - - // Wait simultaneously to ensure all errors are propagated through the same - // future. - await Future.wait([setSourceFuture, preparedFuture]); + // Start position updater to ensure events are pumped on Windows + // This allows getCurrentPosition calls to trigger ProcessPendingTasks + _positionUpdater?.start(); - // Share position once after finished loading - await _positionUpdater?.update(); + try { + final preparedFuture = _onPrepared + .firstWhere((isPrepared) => isPrepared) + .timeout(AudioPlayer.preparationTimeout); + // Need to await the setting the source to propagate immediate errors. + final setSourceFuture = setSource(); + + // Wait simultaneously to ensure all errors are propagated through the + // same future. + await Future.wait([setSourceFuture, preparedFuture]); + + // Share position once after finished loading + await _positionUpdater?.update(); + } on Exception catch (e, stackTrace) { + // Log the error but don't rethrow to prevent app crash + AudioLogger.error( + AudioPlayerException(this, cause: e), + stackTrace, + ); + } } /// Sets the URL to a remote link. diff --git a/packages/audioplayers/package-lock.json b/packages/audioplayers/package-lock.json new file mode 100644 index 000000000..21406f673 --- /dev/null +++ b/packages/audioplayers/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "audioplayers", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/packages/audioplayers/pubspec.yaml b/packages/audioplayers/pubspec.yaml index d98f32808..3bcfe26db 100644 --- a/packages/audioplayers/pubspec.yaml +++ b/packages/audioplayers/pubspec.yaml @@ -41,6 +41,7 @@ dev_dependencies: flame_lint: ^1.4.1 flutter_test: sdk: flutter + mocktail: ^1.0.4 environment: sdk: ^3.6.0 diff --git a/packages/audioplayers/test/audio_cache_test.dart b/packages/audioplayers/test/audio_cache_test.dart deleted file mode 100644 index 13d626e5c..000000000 --- a/packages/audioplayers/test/audio_cache_test.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:audioplayers/audioplayers.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_test/flutter_test.dart'; - -class FakeAudioCache extends AudioCache { - List called = []; - - FakeAudioCache({super.prefix, super.cacheId}); - - @override - Future fetchToMemory(String fileName) async { - called.add(fileName); - return super.fetchToMemory(fileName); - } - - @override - Future loadAsset(String path) async { - return ByteData.sublistView(utf8.encode(path)); - } - - @override - Future getTempDir() async => '/'; -} - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - setUp(() { - AudioCache.fileSystem = MemoryFileSystem.test(); - }); - - group('AudioCache', () { - test('sets cache', () async { - final cache = FakeAudioCache(); - await cache.load('audio.mp3'); - expect(cache.loadedFiles['audio.mp3'], isNotNull); - expect(cache.called, hasLength(1)); - cache.called.clear(); - - await cache.load('audio.mp3'); - expect(cache.called, hasLength(0)); - }); - - test('clear cache', () async { - final cache = FakeAudioCache(); - await cache.load('audio.mp3'); - expect(cache.loadedFiles['audio.mp3'], isNotNull); - await cache.clearAll(); - expect(cache.loadedFiles, {}); - await cache.load('audio.mp3'); - expect(cache.loadedFiles.isNotEmpty, isTrue); - await cache.clear('audio.mp3'); - expect(cache.loadedFiles, {}); - }); - - test('Use different location for two audio caches', () async { - const fileName = 'audio.mp3'; - final cacheA = FakeAudioCache(cacheId: 'cache-path-A'); - await cacheA.load(fileName); - expect(cacheA.loadedFiles[fileName]?.path, '//cache-path-A/audio.mp3'); - - final cacheB = FakeAudioCache(cacheId: 'cache-path-B'); - await cacheB.load(fileName); - expect(cacheB.loadedFiles[fileName]?.path, '//cache-path-B/audio.mp3'); - - await cacheA.clearAll(); - await cacheB.clearAll(); - }); - }); -} diff --git a/packages/audioplayers/test/audio_pool_test.dart b/packages/audioplayers/test/audio_pool_test.dart deleted file mode 100644 index f8ae4b29a..000000000 --- a/packages/audioplayers/test/audio_pool_test.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:audioplayers/audioplayers.dart'; -import 'package:audioplayers_platform_interface/audioplayers_platform_interface.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'audio_cache_test.dart'; -import 'fake_audioplayers_platform.dart'; -import 'fake_global_audioplayers_platform.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('AudioPool', () { - setUp(() { - AudioplayersPlatformInterface.instance = FakeAudioplayersPlatform(); - GlobalAudioplayersPlatformInterface.instance = - FakeGlobalAudioplayersPlatform(); - AudioCache.fileSystem = MemoryFileSystem.test(); - }); - - test('creates instance', () async { - final pool = await AudioPool.createFromAsset( - path: 'audio.mp3', - maxPlayers: 3, - audioCache: FakeAudioCache(), - ); - final stop = await pool.start(); - - expect((pool.source as AssetSource).path, 'audio.mp3'); - expect(pool.audioCache.loadedFiles.keys.first, 'audio.mp3'); - stop(); - expect((pool.source as AssetSource).path, 'audio.mp3'); - }); - - test('multiple players running', () async { - final pool = await AudioPool.createFromAsset( - path: 'audio.mp3', - maxPlayers: 3, - audioCache: FakeAudioCache(), - ); - final stop1 = await pool.start(); - final stop2 = await pool.start(); - final stop3 = await pool.start(); - - expect((pool.source as AssetSource).path, 'audio.mp3'); - expect(pool.audioCache.loadedFiles.keys.first, 'audio.mp3'); - expect(pool.availablePlayers.isEmpty, isTrue); - expect(pool.currentPlayers.length, 3); - - await stop1(); - await stop2(); - await stop3(); - expect(pool.availablePlayers.length, 3); - expect(pool.currentPlayers.isEmpty, isTrue); - }); - - test('keeps the minPlayers/maxPlayers contract', () async { - final pool = await AudioPool.createFromAsset( - path: 'audio.mp3', - maxPlayers: 3, - audioCache: FakeAudioCache(), - ); - final stopFunctions = - await Future.wait(List.generate(5, (_) => pool.start())); - - expect(pool.availablePlayers.isEmpty, isTrue); - expect(pool.currentPlayers.length, 5); - - await stopFunctions[0](); - await stopFunctions[1](); - - expect(pool.availablePlayers.length, 2); - expect(pool.currentPlayers.length, 3); - - await stopFunctions[2](); - await stopFunctions[3](); - await stopFunctions[4](); - - expect(pool.availablePlayers.length, 3); - expect(pool.currentPlayers.isEmpty, isTrue); - }); - }); -} diff --git a/packages/audioplayers/test/audioplayers_test.dart b/packages/audioplayers/test/audioplayers_test.dart index 35c084beb..bf14406c7 100644 --- a/packages/audioplayers/test/audioplayers_test.dart +++ b/packages/audioplayers/test/audioplayers_test.dart @@ -1,159 +1,142 @@ +import 'dart:async'; + import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers_platform_interface/audioplayers_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +// 1. Mock the PlatformInterface +class MockAudioplayersPlatform extends Mock + implements AudioplayersPlatformInterface {} -import 'fake_audioplayers_platform.dart'; -import 'fake_global_audioplayers_platform.dart'; +class MockGlobalAudioplayersPlatform extends Mock + implements GlobalAudioplayersPlatformInterface {} + +class FakeAudioContext extends Fake implements AudioContext {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final globalPlatform = FakeGlobalAudioplayersPlatform(); - GlobalAudioplayersPlatformInterface.instance = globalPlatform; + late MockAudioplayersPlatform mockPlatform; + late MockGlobalAudioplayersPlatform mockGlobalPlatform; + late StreamController eventStreamController; - late FakeAudioplayersPlatform platform; - setUp(() { - platform = FakeAudioplayersPlatform(); - AudioplayersPlatformInterface.instance = platform; + setUpAll(() { + registerFallbackValue(FakeAudioContext()); }); - Future createPlayer({ - required String playerId, - }) async { - final player = AudioPlayer(playerId: playerId); - // Avoid unpredictable position updates - player.positionUpdater = null; - expect(player.source, null); - await player.creatingCompleter.future; - expect(platform.popCall().method, 'create'); - expect(platform.popLastCall().method, 'getEventStream'); - return player; - } - - group('AudioPlayer Methods', () { - late AudioPlayer player; - - setUp(() async { - player = await createPlayer(playerId: 'p1'); - expect(player.source, null); - }); - - test('#setSource and #dispose', () async { - await player.setSource(UrlSource('internet.com/file.mp3')); - expect(platform.popLastCall().method, 'setSourceUrl'); - expect(player.source, isInstanceOf()); - final urlSource = player.source as UrlSource?; - expect(urlSource?.url, 'internet.com/file.mp3'); - - await player.dispose(); - expect(platform.popCall().method, 'stop'); - expect(platform.popCall().method, 'release'); - expect(platform.popLastCall().method, 'dispose'); - expect(player.source, null); - }); - - test('#play', () async { - await player.play(UrlSource('internet.com/file.mp3')); - final call1 = platform.popCall(); - expect(call1.method, 'setSourceUrl'); - expect(call1.value, 'internet.com/file.mp3'); - expect(platform.popLastCall().method, 'resume'); + // Initial configuration before each test + setUp(() { + mockPlatform = MockAudioplayersPlatform(); + mockGlobalPlatform = MockGlobalAudioplayersPlatform(); + eventStreamController = StreamController.broadcast(); + + // Inject our mocks + AudioplayersPlatformInterface.instance = mockPlatform; + GlobalAudioplayersPlatformInterface.instance = mockGlobalPlatform; + + // By default, assume player creation succeeds + when(() => mockPlatform.create(any())).thenAnswer((_) async {}); + // Return our controller's stream + when(() => mockPlatform.getEventStream(any())) + .thenAnswer((_) => eventStreamController.stream); + + // Mock dispose to prevent errors at the end of tests + when(() => mockPlatform.dispose(any())).thenAnswer((_) async { + eventStreamController.close(); }); + when(() => mockPlatform.stop(any())).thenAnswer((_) async {}); + when(() => mockPlatform.release(any())).thenAnswer((_) async {}); + when(() => mockPlatform.getCurrentPosition(any())) + .thenAnswer((_) async => 0); + + // Mock global calls + when(() => mockGlobalPlatform.setGlobalAudioContext(any())) + .thenAnswer((_) async {}); + when(() => mockGlobalPlatform.getGlobalEventStream()) + .thenAnswer((_) => const Stream.empty()); + + // Mock global initialization + when(() => mockGlobalPlatform.init()).thenAnswer((_) async {}); + }); - test('multiple players', () async { - final player2 = await createPlayer(playerId: 'p2'); - - await player.play(UrlSource('internet.com/file.mp3')); - final call1 = platform.popCall(); - expect(call1.id, 'p1'); - expect(call1.method, 'setSourceUrl'); - expect(call1.value, 'internet.com/file.mp3'); - expect(platform.popLastCall().method, 'resume'); - - platform.clear(); - await player.play(UrlSource('internet.com/file.mp3')); - expect(platform.popCall().id, 'p1'); - - platform.clear(); - await player2.play(UrlSource('internet.com/file.mp3')); - expect(platform.popCall().id, 'p2'); - - platform.clear(); - await player.play(UrlSource('internet.com/file.mp3')); - expect(platform.popCall().id, 'p1'); + group('AudioPlayer Logic', () { + test('instantiation', () { + final player = AudioPlayer(); + expect(player, isNotNull); + player.dispose(); }); - test('#resume, #pause and #duration', () async { - await player.setSourceUrl('assets/audio.mp3'); - expect(platform.popLastCall().method, 'setSourceUrl'); - - await player.resume(); - expect(platform.popLastCall().method, 'resume'); + test('play() calls setSource and resume on platform', () async { + final player = AudioPlayer(playerId: 'p1'); + final source = UrlSource('https://example.com/audio.mp3'); + + // Mock expected calls + when( + () => mockPlatform.setSourceUrl( + 'p1', + any(), + isLocal: any(named: 'isLocal'), + mimeType: any(named: 'mimeType'), + ), + ).thenAnswer((_) async { + // IMPORTANT: Simulate platform response saying "It's ready!" + eventStreamController.add( + const AudioEvent( + eventType: AudioEventType.prepared, + isPrepared: true, + ), + ); + }); + + when(() => mockPlatform.resume('p1')).thenAnswer((_) async {}); + + // Action + await player.play(source); + + // Verify + verify( + () => mockPlatform.setSourceUrl( + 'p1', + any(), // Encoded URL + isLocal: false, + ), + ).called(1); - await player.getDuration(); - expect(platform.popLastCall().method, 'getDuration'); + verify(() => mockPlatform.resume('p1')).called(1); - await player.pause(); - expect(platform.popLastCall().method, 'pause'); + // Cleanup + await player.dispose(); }); - test('set #volume, #balance, #playbackRate, #playerMode, #releaseMode', - () async { - await player.setVolume(0.1); - expect(player.volume, 0.1); - expect(platform.popLastCall().method, 'setVolume'); + test('setVolume() delegates to platform', () async { + final player = AudioPlayer(playerId: 'p1'); - await player.setBalance(0.2); - expect(player.balance, 0.2); - expect(platform.popLastCall().method, 'setBalance'); + when(() => mockPlatform.setVolume('p1', 0.5)).thenAnswer((_) async {}); - await player.setPlaybackRate(0.3); - expect(player.playbackRate, 0.3); - expect(platform.popLastCall().method, 'setPlaybackRate'); + await player.setVolume(0.5); - await player.setPlayerMode(PlayerMode.lowLatency); - expect(player.mode, PlayerMode.lowLatency); - expect(platform.popLastCall().method, 'setPlayerMode'); + verify(() => mockPlatform.setVolume('p1', 0.5)).called(1); + expect(player.volume, 0.5); - await player.setReleaseMode(ReleaseMode.loop); - expect(player.releaseMode, ReleaseMode.loop); - expect(platform.popLastCall().method, 'setReleaseMode'); + await player.dispose(); }); - }); - group('AudioPlayers Events', () { - late AudioPlayer player; + test('State is updated when calling resume/pause', () async { + final player = AudioPlayer(playerId: 'p1'); - setUp(() async { - player = await createPlayer(playerId: 'p1'); - expect(player.source, null); - }); + when(() => mockPlatform.resume('p1')).thenAnswer((_) async {}); + when(() => mockPlatform.pause('p1')).thenAnswer((_) async {}); - test('event stream', () async { - final audioEvents = [ - const AudioEvent( - eventType: AudioEventType.duration, - duration: Duration(milliseconds: 98765), - ), - const AudioEvent( - eventType: AudioEventType.log, - logMessage: 'someLogMessage', - ), - const AudioEvent( - eventType: AudioEventType.complete, - ), - const AudioEvent( - eventType: AudioEventType.seekComplete, - ), - ]; + // Test Resume + await player.resume(); + expect(player.state, PlayerState.playing); - expect( - player.eventStream, - emitsInOrder(audioEvents), - ); + // Test Pause + await player.pause(); + expect(player.state, PlayerState.paused); - audioEvents.forEach(platform.eventStreamControllers['p1']!.add); - await platform.eventStreamControllers['p1']!.close(); + await player.dispose(); }); }); } diff --git a/packages/audioplayers/test/fake_audioplayers_platform.dart b/packages/audioplayers/test/fake_audioplayers_platform.dart deleted file mode 100644 index 40c49a44f..000000000 --- a/packages/audioplayers/test/fake_audioplayers_platform.dart +++ /dev/null @@ -1,166 +0,0 @@ -import 'dart:async'; - -import 'package:audioplayers_platform_interface/audioplayers_platform_interface.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -class FakeCall { - final String id; - final String method; - final Object? value; - - FakeCall({required this.id, required this.method, this.value}); - - @override - String toString() => 'FakeCall(id: $id, method: $method, value: $value)'; -} - -class FakeAudioplayersPlatform extends AudioplayersPlatformInterface { - List calls = []; - - Map> eventStreamControllers = {}; - - void clear() { - calls.clear(); - } - - FakeCall popCall() { - return calls.removeAt(0); - } - - FakeCall popLastCall() { - expect(calls, hasLength(1)); - return popCall(); - } - - @override - Future create(String playerId) async { - calls.add(FakeCall(id: playerId, method: 'create')); - eventStreamControllers[playerId] = StreamController.broadcast(); - } - - @override - Future dispose(String playerId) async { - calls.add(FakeCall(id: playerId, method: 'dispose')); - eventStreamControllers[playerId]?.close(); - } - - @override - Future emitError(String playerId, String code, String message) async { - calls.add(FakeCall(id: playerId, method: 'emitError')); - } - - @override - Future emitLog(String playerId, String message) async { - calls.add(FakeCall(id: playerId, method: 'emitLog')); - } - - @override - Future getCurrentPosition(String playerId) async { - calls.add(FakeCall(id: playerId, method: 'getCurrentPosition')); - return 0; - } - - @override - Future getDuration(String playerId) async { - calls.add(FakeCall(id: playerId, method: 'getDuration')); - return 0; - } - - @override - Future pause(String playerId) async { - calls.add(FakeCall(id: playerId, method: 'pause')); - } - - @override - Future release(String playerId) async { - calls.add(FakeCall(id: playerId, method: 'release')); - } - - @override - Future resume(String playerId) async { - calls.add(FakeCall(id: playerId, method: 'resume')); - } - - @override - Future seek(String playerId, Duration position) async { - calls.add(FakeCall(id: playerId, method: 'seek', value: position)); - } - - @override - Future setAudioContext( - String playerId, - AudioContext audioContext, - ) async { - calls.add( - FakeCall(id: playerId, method: 'setAudioContext', value: audioContext), - ); - } - - @override - Future setBalance(String playerId, double balance) async { - calls.add(FakeCall(id: playerId, method: 'setBalance', value: balance)); - } - - @override - Future setPlaybackRate(String playerId, double playbackRate) async { - calls.add( - FakeCall(id: playerId, method: 'setPlaybackRate', value: playbackRate), - ); - } - - @override - Future setPlayerMode(String playerId, PlayerMode playerMode) async { - calls.add( - FakeCall(id: playerId, method: 'setPlayerMode', value: playerMode), - ); - } - - @override - Future setReleaseMode(String playerId, ReleaseMode releaseMode) async { - calls.add( - FakeCall(id: playerId, method: 'setReleaseMode', value: releaseMode), - ); - } - - @override - Future setSourceBytes( - String playerId, - Uint8List bytes, { - String? mimeType, - }) async { - calls.add(FakeCall(id: playerId, method: 'setSourceBytes', value: bytes)); - eventStreamControllers[playerId]?.add( - const AudioEvent(eventType: AudioEventType.prepared, isPrepared: true), - ); - } - - @override - Future setSourceUrl( - String playerId, - String url, { - bool? isLocal, - String? mimeType, - }) async { - calls.add(FakeCall(id: playerId, method: 'setSourceUrl', value: url)); - eventStreamControllers[playerId]?.add( - const AudioEvent(eventType: AudioEventType.prepared, isPrepared: true), - ); - } - - @override - Future setVolume(String playerId, double volume) async { - calls.add(FakeCall(id: playerId, method: 'setVolume', value: volume)); - } - - @override - Future stop(String playerId) async { - calls.add(FakeCall(id: playerId, method: 'stop')); - } - - @override - Stream getEventStream(String playerId) { - calls.add(FakeCall(id: playerId, method: 'getEventStream')); - return eventStreamControllers[playerId]!.stream; - } -} diff --git a/packages/audioplayers/test/fake_global_audioplayers_platform.dart b/packages/audioplayers/test/fake_global_audioplayers_platform.dart deleted file mode 100644 index af3aa1104..000000000 --- a/packages/audioplayers/test/fake_global_audioplayers_platform.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:async'; - -import 'package:audioplayers_platform_interface/audioplayers_platform_interface.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -class FakeGlobalCall { - final String method; - final Object? value; - - FakeGlobalCall({required this.method, this.value}); -} - -class FakeGlobalAudioplayersPlatform - extends GlobalAudioplayersPlatformInterface { - List calls = []; - StreamController eventStreamController = - StreamController.broadcast(); - - void clear() { - calls.clear(); - } - - FakeGlobalCall popCall() { - return calls.removeAt(0); - } - - FakeGlobalCall popLastCall() { - expect(calls, hasLength(1)); - return popCall(); - } - - @override - Future init() async { - calls.add(FakeGlobalCall(method: 'init')); - } - - @override - Future setGlobalAudioContext(AudioContext ctx) async { - calls.add(FakeGlobalCall(method: 'setGlobalAudioContext', value: ctx)); - } - - @override - Future emitGlobalLog(String message) async { - calls.add(FakeGlobalCall(method: 'emitGlobalLog')); - eventStreamController.add( - GlobalAudioEvent( - eventType: GlobalAudioEventType.log, - logMessage: message, - ), - ); - } - - @override - Future emitGlobalError(String code, String message) async { - calls.add(FakeGlobalCall(method: 'emitGlobalError')); - eventStreamController - .addError(PlatformException(code: code, message: message)); - } - - @override - Stream getGlobalEventStream() { - calls.add(FakeGlobalCall(method: 'getGlobalEventStream')); - return eventStreamController.stream; - } - - Future dispose() async { - calls.add(FakeGlobalCall(method: 'globalDispose')); - eventStreamController.close(); - } -} diff --git a/packages/audioplayers/test/global_audioplayers_test.dart b/packages/audioplayers/test/global_audioplayers_test.dart deleted file mode 100644 index 5e930cb52..000000000 --- a/packages/audioplayers/test/global_audioplayers_test.dart +++ /dev/null @@ -1,79 +0,0 @@ -//ignore_for_file: avoid_redundant_argument_values -import 'package:audioplayers/audioplayers.dart'; -import 'package:audioplayers_platform_interface/audioplayers_platform_interface.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'fake_global_audioplayers_platform.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final globalPlatform = FakeGlobalAudioplayersPlatform(); - GlobalAudioplayersPlatformInterface.instance = globalPlatform; - - late GlobalAudioScope globalScope; - - test('test getGlobalEventStream', () async { - // Global scope can only be initialized once statically, as changing it - // while connected to native platform can lead to inconsistencies. - globalScope = AudioPlayer.global; - expect(globalPlatform.popLastCall().method, 'getGlobalEventStream'); - }); - - group('Global Methods', () { - setUp(() { - // Ensure that globalScope was initialized and calls are reset. - globalScope = AudioPlayer.global; - globalPlatform.clear(); - }); - - /// Note that the [AudioContextIOS.category] has to be - /// [AVAudioSessionCategory.playback] to default the audio to the receiver - /// (e.g. built-in speakers or BT-device, if connected). - /// If using [AVAudioSessionCategory.playAndRecord] the audio will come from - /// the earpiece unless [AVAudioSessionOptions.defaultToSpeaker] is used. - test('set AudioContext', () async { - await globalScope.setAudioContext(AudioContext()); - var call = globalPlatform.popCall(); - expect(call.method, 'init'); - call = globalPlatform.popLastCall(); - expect(call.method, 'setGlobalAudioContext'); - expect( - call.value, - AudioContext( - android: const AudioContextAndroid( - isSpeakerphoneOn: false, - audioMode: AndroidAudioMode.normal, - stayAwake: false, - contentType: AndroidContentType.music, - usageType: AndroidUsageType.media, - audioFocus: AndroidAudioFocus.gain, - ), - iOS: AudioContextIOS( - category: AVAudioSessionCategory.playback, - options: const {}, - ), - ), - ); - }); - }); - - group('Global Events', () { - test('global event stream', () async { - final globalEvents = [ - const GlobalAudioEvent( - eventType: GlobalAudioEventType.log, - logMessage: 'someLogMessage', - ), - ]; - - expect( - globalScope.eventStream, - emitsInOrder(globalEvents), - ); - - globalEvents.forEach(globalPlatform.eventStreamController.add); - await globalPlatform.eventStreamController.close(); - }); - }); -} diff --git a/packages/audioplayers/test/logger_test.dart b/packages/audioplayers/test/logger_test.dart deleted file mode 100644 index c9c7f5f86..000000000 --- a/packages/audioplayers/test/logger_test.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'dart:async'; - -import 'package:audioplayers/audioplayers.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final printZone = OverridePrint(); - - group('Logger', () { - setUp(printZone.clear); - - test( - 'when set to INFO everything is logged', - printZone.overridePrint(() { - AudioLogger.logLevel = AudioLogLevel.info; - - AudioLogger.log('info'); - AudioLogger.error('error'); - - expect(printZone.logs, [ - 'AudioPlayers Log: info', - '\x1B[31mAudioPlayers throw: error\x1B[0m', - ]); - }), - ); - - test( - 'when set to ERROR only errors are logged', - printZone.overridePrint(() { - AudioLogger.logLevel = AudioLogLevel.error; - - AudioLogger.log('info'); - AudioLogger.error('error'); - - expect(printZone.logs, [ - '\x1B[31mAudioPlayers throw: error\x1B[0m', - ]); - }), - ); - - test( - 'when set to NONE nothing is logged', - printZone.overridePrint(() { - AudioLogger.logLevel = AudioLogLevel.none; - - AudioLogger.log('info'); - AudioLogger.error('error'); - - expect(printZone.logs, []); - }), - ); - }); -} - -class OverridePrint { - final logs = []; - - void clear() => logs.clear(); - - void Function() overridePrint(void Function() testFn) { - return () { - final spec = ZoneSpecification( - print: (_, __, ___, String msg) { - // Add to log instead of printing to stdout - logs.add(msg); - }, - ); - return Zone.current.fork(specification: spec).run(testFn); - }; - } -} diff --git a/packages/audioplayers/test/uri_coder_test.dart b/packages/audioplayers/test/uri_coder_test.dart deleted file mode 100644 index 8170c4d26..000000000 --- a/packages/audioplayers/test/uri_coder_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:audioplayers/src/uri_ext.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('UriCoder', () { - test( - 'Encode Special Character', - () { - const uri = '/coins_non_ascii_и.wav'; - final encoded = UriCoder.encodeOnce(uri); - expect(encoded, '/coins_non_ascii_%D0%B8.wav'); - }, - ); - test( - 'Encode Space', - () { - const uri = '/coins .wav'; - final encoded = UriCoder.encodeOnce(uri); - expect(encoded, '/coins%20.wav'); - }, - ); - test( - 'Already encoded Character', - () { - const uri = 'https://myurl/audio%2F_music.mp4?alt=media&token=abc'; - final encoded = UriCoder.encodeOnce(uri); - expect(encoded, uri); - }, - ); - test( - 'Encoded and decoded are the same', - () { - const uri = 'https://myurl/audio'; - final encoded = UriCoder.encodeOnce(uri); - expect(encoded, uri); - }, - ); - }); -} diff --git a/packages/audioplayers_android/.metadata b/packages/audioplayers_android/.metadata index 434f1e5eb..7a9751c8c 100644 --- a/packages/audioplayers_android/.metadata +++ b/packages/audioplayers_android/.metadata @@ -4,7 +4,27 @@ # This file should be version controlled and should not be manually edited. version: - revision: a0860f6e87ba4f9031bee4d6f56c08b970606bee - channel: dev + revision: "f6ff1529fd6d8af5f706051d9251ac9231c83407" + channel: "stable" project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + - platform: android + create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/audioplayers_android/android/build.gradle b/packages/audioplayers_android/android/build.gradle index 6651c6ce2..de9267fb7 100644 --- a/packages/audioplayers_android/android/build.gradle +++ b/packages/audioplayers_android/android/build.gradle @@ -2,8 +2,8 @@ group 'xyz.luan.audioplayers' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.7.10' - ext.coroutines_version = '1.6.4' + ext.kotlin_version = '2.2.21' + ext.coroutines_version = '1.10.2' repositories { google() @@ -11,9 +11,9 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.3.1' + classpath 'com.android.tools.build:gradle:8.11.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "de.mannodermaus.gradle.plugins:android-junit5:1.7.1.1" + classpath "de.mannodermaus.gradle.plugins:android-junit5:1.10.2.0" } } @@ -29,7 +29,7 @@ apply plugin: 'kotlin-android' apply plugin: 'de.mannodermaus.android-junit5' android { - compileSdk 35 + compileSdk 36 // Conditional for compatibility with AGP <4.2. if (project.android.hasProperty('namespace')) { @@ -37,12 +37,12 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } sourceSets { @@ -68,12 +68,12 @@ allprojects { } dependencies { - implementation "androidx.core:core-ktx:1.9.0" // Do not pump unless dropping support for AGP7 + implementation 'androidx.core:core-ktx:1.17.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" - testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0' - testImplementation 'org.assertj:assertj-core:3.23.1' + testImplementation 'org.junit.jupiter:junit-jupiter:6.0.1' + testImplementation 'org.assertj:assertj-core:3.27.6' } repositories { mavenCentral() diff --git a/packages/audioplayers_android/example/.gitignore b/packages/audioplayers_android/example/.gitignore new file mode 100644 index 000000000..3820a95c6 --- /dev/null +++ b/packages/audioplayers_android/example/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/audioplayers_android/example/README.md b/packages/audioplayers_android/example/README.md new file mode 100644 index 000000000..16b14767f --- /dev/null +++ b/packages/audioplayers_android/example/README.md @@ -0,0 +1,16 @@ +# audioplayers_android_example + +Demonstrates how to use the audioplayers_android plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/audioplayers_android/example/analysis_options.yaml b/packages/audioplayers_android/example/analysis_options.yaml new file mode 100644 index 000000000..0d2902135 --- /dev/null +++ b/packages/audioplayers_android/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/audioplayers_android/example/android/.gitignore b/packages/audioplayers_android/example/android/.gitignore new file mode 100644 index 000000000..be3943c96 --- /dev/null +++ b/packages/audioplayers_android/example/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/packages/audioplayers_android/example/android/app/build.gradle.kts b/packages/audioplayers_android/example/android/app/build.gradle.kts new file mode 100644 index 000000000..d5fe784eb --- /dev/null +++ b/packages/audioplayers_android/example/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.audioplayers_android_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.audioplayers_android_example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/packages/audioplayers_android/example/android/app/src/debug/AndroidManifest.xml b/packages/audioplayers_android/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..399f6981d --- /dev/null +++ b/packages/audioplayers_android/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/audioplayers_android/example/android/app/src/main/AndroidManifest.xml b/packages/audioplayers_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..49f832d12 --- /dev/null +++ b/packages/audioplayers_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/audioplayers_android/example/android/app/src/main/kotlin/com/example/audioplayers_android_example/MainActivity.kt b/packages/audioplayers_android/example/android/app/src/main/kotlin/com/example/audioplayers_android_example/MainActivity.kt new file mode 100644 index 000000000..76e953ae2 --- /dev/null +++ b/packages/audioplayers_android/example/android/app/src/main/kotlin/com/example/audioplayers_android_example/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.audioplayers_android_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/packages/audioplayers_android/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/audioplayers_android/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000..f74085f3f --- /dev/null +++ b/packages/audioplayers_android/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/audioplayers_android/example/android/app/src/main/res/drawable/launch_background.xml b/packages/audioplayers_android/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000..304732f88 --- /dev/null +++ b/packages/audioplayers_android/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/audioplayers_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/audioplayers_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..db77bb4b7 Binary files /dev/null and b/packages/audioplayers_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/audioplayers_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/audioplayers_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..17987b79b Binary files /dev/null and b/packages/audioplayers_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/audioplayers_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/audioplayers_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..09d439148 Binary files /dev/null and b/packages/audioplayers_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/audioplayers_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/audioplayers_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..d5f1c8d34 Binary files /dev/null and b/packages/audioplayers_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/audioplayers_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/audioplayers_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..4d6372eeb Binary files /dev/null and b/packages/audioplayers_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/audioplayers_android/example/android/app/src/main/res/values-night/styles.xml b/packages/audioplayers_android/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000..06952be74 --- /dev/null +++ b/packages/audioplayers_android/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/audioplayers_android/example/android/app/src/main/res/values/styles.xml b/packages/audioplayers_android/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..cb1ef8805 --- /dev/null +++ b/packages/audioplayers_android/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/audioplayers_android/example/android/app/src/profile/AndroidManifest.xml b/packages/audioplayers_android/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000..399f6981d --- /dev/null +++ b/packages/audioplayers_android/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/audioplayers_android/example/android/build.gradle.kts b/packages/audioplayers_android/example/android/build.gradle.kts new file mode 100644 index 000000000..dbee657bb --- /dev/null +++ b/packages/audioplayers_android/example/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/packages/audioplayers_android/example/android/gradle.properties b/packages/audioplayers_android/example/android/gradle.properties new file mode 100644 index 000000000..fbee1d8cd --- /dev/null +++ b/packages/audioplayers_android/example/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/packages/audioplayers_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/audioplayers_android/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..e4ef43fb9 --- /dev/null +++ b/packages/audioplayers_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/packages/audioplayers_android/example/android/settings.gradle.kts b/packages/audioplayers_android/example/android/settings.gradle.kts new file mode 100644 index 000000000..ca7fe065c --- /dev/null +++ b/packages/audioplayers_android/example/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/packages/audioplayers_android/example/lib/main.dart b/packages/audioplayers_android/example/lib/main.dart new file mode 100644 index 000000000..0173f063c --- /dev/null +++ b/packages/audioplayers_android/example/lib/main.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:audioplayers_android/audioplayers_android.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + String _platformVersion = 'Unknown'; + final _audioplayersAndroidPlugin = AudioplayersAndroid(); + + @override + void initState() { + super.initState(); + initPlatformState(); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future initPlatformState() async { + String platformVersion; + // Platform messages may fail, so we use a try/catch PlatformException. + // We also handle the message potentially returning null. + try { + platformVersion = + await _audioplayersAndroidPlugin.getPlatformVersion() ?? + 'Unknown platform version'; + } on PlatformException { + platformVersion = 'Failed to get platform version.'; + } + + // If the widget was removed from the tree while the asynchronous platform + // message was in flight, we want to discard the reply rather than calling + // setState to update our non-existent appearance. + if (!mounted) return; + + setState(() { + _platformVersion = platformVersion; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar(title: const Text('Plugin example app')), + body: Center(child: Text('Running on: $_platformVersion\n')), + ), + ); + } +} diff --git a/packages/audioplayers_android/example/pubspec.yaml b/packages/audioplayers_android/example/pubspec.yaml new file mode 100644 index 000000000..4d4d0f3c2 --- /dev/null +++ b/packages/audioplayers_android/example/pubspec.yaml @@ -0,0 +1,85 @@ +name: audioplayers_android_example +description: "Demonstrates how to use the audioplayers_android 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 + +environment: + sdk: ^3.10.4 + +# 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 + + audioplayers_android: + # When depending on this package from a real application you should use: + # audioplayers_android: ^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. + cupertino_icons: ^1.0.8 + +dev_dependencies: + integration_test: + 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: ^6.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 + + # 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/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # 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/to/font-from-package diff --git a/packages/audioplayers_android/lib/audioplayers_android.dart b/packages/audioplayers_android/lib/audioplayers_android.dart new file mode 100644 index 000000000..61927cd93 --- /dev/null +++ b/packages/audioplayers_android/lib/audioplayers_android.dart @@ -0,0 +1,7 @@ +import 'package:audioplayers_android/audioplayers_android_platform_interface.dart'; + +class AudioplayersAndroid { + Future getPlatformVersion() { + return AudioplayersAndroidPlatform.instance.getPlatformVersion(); + } +} diff --git a/packages/audioplayers_android/lib/audioplayers_android_method_channel.dart b/packages/audioplayers_android/lib/audioplayers_android_method_channel.dart new file mode 100644 index 000000000..37dd21c3f --- /dev/null +++ b/packages/audioplayers_android/lib/audioplayers_android_method_channel.dart @@ -0,0 +1,17 @@ +import 'package:audioplayers_android/audioplayers_android_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +/// An implementation of [AudioplayersAndroidPlatform] that uses method channels +class MethodChannelAudioplayersAndroid extends AudioplayersAndroidPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('audioplayers_android'); + + @override + Future getPlatformVersion() async { + final version = + await methodChannel.invokeMethod('getPlatformVersion'); + return version; + } +} diff --git a/packages/audioplayers_android/lib/audioplayers_android_platform_interface.dart b/packages/audioplayers_android/lib/audioplayers_android_platform_interface.dart new file mode 100644 index 000000000..66aecbac6 --- /dev/null +++ b/packages/audioplayers_android/lib/audioplayers_android_platform_interface.dart @@ -0,0 +1,29 @@ +import 'package:audioplayers_android/audioplayers_android_method_channel.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +abstract class AudioplayersAndroidPlatform extends PlatformInterface { + /// Constructs a AudioplayersAndroidPlatform. + AudioplayersAndroidPlatform() : super(token: _token); + + static final Object _token = Object(); + + static AudioplayersAndroidPlatform _instance = + MethodChannelAudioplayersAndroid(); + + /// The default instance of [AudioplayersAndroidPlatform] to use. + /// + /// Defaults to [MethodChannelAudioplayersAndroid]. + static AudioplayersAndroidPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [AudioplayersAndroidPlatform] when + /// they register themselves. + static set instance(AudioplayersAndroidPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future getPlatformVersion() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } +} diff --git a/packages/audioplayers_android/package-lock.json b/packages/audioplayers_android/package-lock.json new file mode 100644 index 000000000..75f51f794 --- /dev/null +++ b/packages/audioplayers_android/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "audioplayers_android", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/packages/audioplayers_android/pubspec.yaml b/packages/audioplayers_android/pubspec.yaml index fd5ebddb0..3c32dd1a6 100644 --- a/packages/audioplayers_android/pubspec.yaml +++ b/packages/audioplayers_android/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: audioplayers_platform_interface: ^7.1.1 flutter: sdk: flutter + plugin_platform_interface: ^2.1.8 dev_dependencies: flame_lint: ^1.4.1 diff --git a/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/checksums/checksums.lock b/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/checksums/checksums.lock new file mode 100644 index 000000000..d3faa129d Binary files /dev/null and b/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/checksums/checksums.lock differ diff --git a/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/checksums/md5-checksums.bin b/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/checksums/md5-checksums.bin new file mode 100644 index 000000000..ed7edd552 Binary files /dev/null and b/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/checksums/md5-checksums.bin differ diff --git a/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/checksums/sha1-checksums.bin b/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/checksums/sha1-checksums.bin new file mode 100644 index 000000000..bc87b8aa7 Binary files /dev/null and b/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/checksums/sha1-checksums.bin differ diff --git a/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/executionHistory/executionHistory.bin b/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/executionHistory/executionHistory.bin new file mode 100644 index 000000000..7e8e5c8f4 Binary files /dev/null and b/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/executionHistory/executionHistory.bin differ diff --git a/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/executionHistory/executionHistory.lock b/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/executionHistory/executionHistory.lock new file mode 100644 index 000000000..e8da0162e Binary files /dev/null and b/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/executionHistory/executionHistory.lock differ diff --git a/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/fileChanges/last-build.bin b/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/fileChanges/last-build.bin new file mode 100644 index 000000000..f76dd238a Binary files /dev/null and b/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/fileChanges/last-build.bin differ diff --git a/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/fileHashes/fileHashes.bin b/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/fileHashes/fileHashes.bin new file mode 100644 index 000000000..426884b10 Binary files /dev/null and b/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/fileHashes/fileHashes.bin differ diff --git a/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/fileHashes/fileHashes.lock b/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/fileHashes/fileHashes.lock new file mode 100644 index 000000000..5c46d7fe8 Binary files /dev/null and b/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/fileHashes/fileHashes.lock differ diff --git a/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/fileHashes/resourceHashesCache.bin b/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/fileHashes/resourceHashesCache.bin new file mode 100644 index 000000000..19d507621 Binary files /dev/null and b/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/fileHashes/resourceHashesCache.bin differ diff --git a/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/gc.properties b/packages/audioplayers_android_exo/android/.gradle/9.0-milestone-1/gc.properties new file mode 100644 index 000000000..e69de29bb diff --git a/packages/audioplayers_android_exo/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/packages/audioplayers_android_exo/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 000000000..029849814 Binary files /dev/null and b/packages/audioplayers_android_exo/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/packages/audioplayers_android_exo/android/.gradle/buildOutputCleanup/cache.properties b/packages/audioplayers_android_exo/android/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 000000000..7960383d8 --- /dev/null +++ b/packages/audioplayers_android_exo/android/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Tue Dec 30 16:16:15 CET 2025 +gradle.version=9.0-milestone-1 diff --git a/packages/audioplayers_android_exo/android/.gradle/buildOutputCleanup/outputFiles.bin b/packages/audioplayers_android_exo/android/.gradle/buildOutputCleanup/outputFiles.bin new file mode 100644 index 000000000..6e06e8161 Binary files /dev/null and b/packages/audioplayers_android_exo/android/.gradle/buildOutputCleanup/outputFiles.bin differ diff --git a/packages/audioplayers_android_exo/android/.gradle/config.properties b/packages/audioplayers_android_exo/android/.gradle/config.properties new file mode 100644 index 000000000..abb0036e8 --- /dev/null +++ b/packages/audioplayers_android_exo/android/.gradle/config.properties @@ -0,0 +1,2 @@ +#Tue Dec 30 16:16:08 CET 2025 +java.home=C\:\\Devs\\Android Studio\\jbr diff --git a/packages/audioplayers_android_exo/android/.gradle/file-system.probe b/packages/audioplayers_android_exo/android/.gradle/file-system.probe new file mode 100644 index 000000000..04c34bbce Binary files /dev/null and b/packages/audioplayers_android_exo/android/.gradle/file-system.probe differ diff --git a/packages/audioplayers_android_exo/android/.gradle/vcs-1/gc.properties b/packages/audioplayers_android_exo/android/.gradle/vcs-1/gc.properties new file mode 100644 index 000000000..e69de29bb diff --git a/packages/audioplayers_android_exo/android/build.gradle b/packages/audioplayers_android_exo/android/build.gradle index 8d03d24b8..41e1d42bf 100644 --- a/packages/audioplayers_android_exo/android/build.gradle +++ b/packages/audioplayers_android_exo/android/build.gradle @@ -2,8 +2,8 @@ group 'xyz.luan.audioplayers' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.7.10' - ext.coroutines_version = '1.6.4' + ext.kotlin_version = '2.2.21' + ext.coroutines_version = '1.10.2' repositories { google() @@ -11,9 +11,9 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.3.1' + classpath 'com.android.tools.build:gradle:8.13.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "de.mannodermaus.gradle.plugins:android-junit5:1.7.1.1" + classpath "de.mannodermaus.gradle.plugins:android-junit5:1.14.0.0" } } @@ -29,7 +29,7 @@ apply plugin: 'kotlin-android' apply plugin: 'de.mannodermaus.android-junit5' android { - compileSdk 35 + compileSdk 36 // Conditional for compatibility with AGP <4.2. if (project.android.hasProperty('namespace')) { @@ -37,12 +37,12 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } sourceSets { @@ -68,19 +68,19 @@ allprojects { } dependencies { - def exoplayer_version = "1.5.1" + def exoplayer_version = '1.9.0' implementation "androidx.media3:media3-exoplayer:${exoplayer_version}" implementation "androidx.media3:media3-exoplayer-hls:${exoplayer_version}" implementation "androidx.media3:media3-exoplayer-dash:${exoplayer_version}" implementation "androidx.media3:media3-exoplayer-rtsp:${exoplayer_version}" implementation "androidx.media3:media3-exoplayer-smoothstreaming:${exoplayer_version}" - implementation "androidx.core:core-ktx:1.9.0" // Do not pump unless dropping support for AGP7 + implementation 'androidx.core:core-ktx:1.17.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" - testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0' - testImplementation 'org.assertj:assertj-core:3.23.1' + testImplementation 'org.junit.jupiter:junit-jupiter:6.0.1' + testImplementation 'org.assertj:assertj-core:3.27.6' } repositories { mavenCentral() diff --git a/packages/audioplayers_android_exo/android/build/reports/problems/problems-report.html b/packages/audioplayers_android_exo/android/build/reports/problems/problems-report.html new file mode 100644 index 000000000..eaa3d89e1 --- /dev/null +++ b/packages/audioplayers_android_exo/android/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/packages/audioplayers_android_exo/android/gradle.properties b/packages/audioplayers_android_exo/android/gradle.properties index a8da7d998..94adc3a3f 120000 --- a/packages/audioplayers_android_exo/android/gradle.properties +++ b/packages/audioplayers_android_exo/android/gradle.properties @@ -1 +1,3 @@ -../../audioplayers_android/android/gradle.properties \ No newline at end of file +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/audioplayers_android_exo/android/gradle/wrapper/gradle-wrapper.jar b/packages/audioplayers_android_exo/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..980502d16 Binary files /dev/null and b/packages/audioplayers_android_exo/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/packages/audioplayers_android_exo/android/gradle/wrapper/gradle-wrapper.properties b/packages/audioplayers_android_exo/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..128196a7a --- /dev/null +++ b/packages/audioplayers_android_exo/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-milestone-1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/packages/audioplayers_android_exo/android/gradlew b/packages/audioplayers_android_exo/android/gradlew new file mode 100644 index 000000000..faf93008b --- /dev/null +++ b/packages/audioplayers_android_exo/android/gradlew @@ -0,0 +1,251 @@ +#!/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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# 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/platforms/jvm/plugins-application/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 -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || 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, 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/packages/audioplayers_android_exo/android/gradlew.bat b/packages/audioplayers_android_exo/android/gradlew.bat new file mode 100644 index 000000000..9d21a2183 --- /dev/null +++ b/packages/audioplayers_android_exo/android/gradlew.bat @@ -0,0 +1,94 @@ +@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 +@rem SPDX-License-Identifier: Apache-2.0 +@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. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +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/packages/audioplayers_android_exo/android/local.properties b/packages/audioplayers_android_exo/android/local.properties new file mode 100644 index 000000000..f39b682af --- /dev/null +++ b/packages/audioplayers_android_exo/android/local.properties @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Tue Dec 30 16:16:08 CET 2025 +sdk.dir=C\:\\Users\\Actur\\AppData\\Local\\Android\\Sdk diff --git a/packages/audioplayers_linux/linux/audio_player.cc b/packages/audioplayers_linux/linux/audio_player.cc index afe235168..edcdc8750 100644 --- a/packages/audioplayers_linux/linux/audio_player.cc +++ b/packages/audioplayers_linux/linux/audio_player.cc @@ -1,5 +1,6 @@ #include "audio_player.h" #include +#include #define STR_LINK_TROUBLESHOOTING \ "https://github.com/bluefireteam/audioplayers/blob/main/troubleshooting.md" @@ -10,11 +11,20 @@ AudioPlayer::AudioPlayer(std::string playerId, // GStreamer lib only needs to be initialized once, but doing it while // registering the plugin can be problematic as it likely needs a GUI to be // present. Calling it multiple times is fine. - gst_init(NULL, NULL); + GError* error = nullptr; + if (!gst_init_check(nullptr, nullptr, &error)) { + std::string errStr = "Failed to initialize GStreamer"; + if (error) { + errStr += ": "; + errStr += error->message; + g_error_free(error); + } + throw errStr; + } playbin = gst_element_factory_make("playbin", NULL); if (!playbin) { - throw "Not all elements could be created."; + throw "GStreamer element 'playbin' could not be created. Make sure you have GStreamer and all necessary plugins installed. For troubleshooting, see: " STR_LINK_TROUBLESHOOTING; } // Setup stereo balance controller @@ -95,40 +105,48 @@ void AudioPlayer::ReleaseMediaSource() { gboolean AudioPlayer::OnBusMessage(GstBus* bus, GstMessage* message, AudioPlayer* data) { - switch (GST_MESSAGE_TYPE(message)) { - case GST_MESSAGE_ERROR: { - GError* err; - gchar* debug; - - gst_message_parse_error(message, &err, &debug); - data->OnMediaError(err, debug); - g_error_free(err); - g_free(debug); - break; - } - case GST_MESSAGE_STATE_CHANGED: - GstState old_state, new_state; - - gst_message_parse_state_changed(message, &old_state, &new_state, NULL); - data->OnMediaStateChange(GST_MESSAGE_SRC(message), &old_state, - &new_state); - break; - case GST_MESSAGE_EOS: - data->OnPlaybackEnded(); - break; - case GST_MESSAGE_DURATION_CHANGED: - data->OnDurationUpdate(); - break; - case GST_MESSAGE_ASYNC_DONE: - if (!data->_isSeekCompleted) { - data->OnSeekCompleted(); - data->_isSeekCompleted = true; + try { + switch (GST_MESSAGE_TYPE(message)) { + case GST_MESSAGE_ERROR: { + GError* err; + gchar* debug; + + gst_message_parse_error(message, &err, &debug); + data->OnMediaError(err, debug); + g_error_free(err); + g_free(debug); + break; } - break; - default: - // For more GstMessage types see: - // https://gstreamer.freedesktop.org/documentation/gstreamer/gstmessage.html?gi-language=c#enumerations - break; + case GST_MESSAGE_STATE_CHANGED: + GstState old_state, new_state; + + gst_message_parse_state_changed(message, &old_state, &new_state, NULL); + data->OnMediaStateChange(GST_MESSAGE_SRC(message), &old_state, + &new_state); + break; + case GST_MESSAGE_EOS: + data->OnPlaybackEnded(); + break; + case GST_MESSAGE_DURATION_CHANGED: + data->OnDurationUpdate(); + break; + case GST_MESSAGE_ASYNC_DONE: + if (!data->_isSeekCompleted) { + data->OnSeekCompleted(); + data->_isSeekCompleted = true; + } + break; + default: + // For more GstMessage types see: + // https://gstreamer.freedesktop.org/documentation/gstreamer/gstmessage.html?gi-language=c#enumerations + break; + } + } catch (const char* error) { + data->OnLog(error); + } catch (const std::exception& e) { + data->OnLog(e.what()); + } catch (...) { + data->OnLog("Unknown error in OnBusMessage"); } // Continue watching for messages @@ -444,8 +462,9 @@ void AudioPlayer::Resume() { } void AudioPlayer::Dispose() { - if (!playbin) - throw "Player was already disposed (Dispose)"; + if (!playbin) { + return; + } ReleaseMediaSource(); if (bus) { diff --git a/packages/audioplayers_linux/linux/audioplayers_linux_plugin.cc b/packages/audioplayers_linux/linux/audioplayers_linux_plugin.cc index 016e148b8..43f029978 100644 --- a/packages/audioplayers_linux/linux/audioplayers_linux_plugin.cc +++ b/packages/audioplayers_linux/linux/audioplayers_linux_plugin.cc @@ -106,37 +106,43 @@ static void audioplayers_linux_plugin_handle_method_call( const gchar* method = fl_method_call_get_name(method_call); FlValue* args = fl_method_call_get_args(method_call); - auto flPlayerId = fl_value_lookup_string(args, "playerId"); - if (flPlayerId == nullptr) { - response = FL_METHOD_RESPONSE(fl_method_error_response_new( - "LinuxAudioError", "Call missing mandatory parameter playerId.", - nullptr)); - fl_method_call_respond(method_call, response, nullptr); - return; - } - auto playerId = std::string(fl_value_get_string(flPlayerId)); + try { + auto flPlayerId = fl_value_lookup_string(args, "playerId"); + if (flPlayerId == nullptr) { + response = FL_METHOD_RESPONSE(fl_method_error_response_new( + "LinuxAudioError", "Call missing mandatory parameter playerId.", + nullptr)); + fl_method_call_respond(method_call, response, nullptr); + return; + } + if (fl_value_get_type(flPlayerId) != FL_VALUE_TYPE_STRING) { + response = FL_METHOD_RESPONSE(fl_method_error_response_new( + "LinuxAudioError", "Parameter playerId must be a string.", nullptr)); + fl_method_call_respond(method_call, response, nullptr); + return; + } + auto playerId = std::string(fl_value_get_string(flPlayerId)); - if (strcmp(method, "create") == 0) { - audioplayers_linux_plugin_create_player(playerId); - response = - FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_int(1))); - fl_method_call_respond(method_call, response, nullptr); - return; - } + if (strcmp(method, "create") == 0) { + audioplayers_linux_plugin_create_player(playerId); + response = FL_METHOD_RESPONSE( + fl_method_success_response_new(fl_value_new_int(1))); + fl_method_call_respond(method_call, response, nullptr); + return; + } - auto player = audioplayers_linux_plugin_get_player(playerId); - if (!player) { - response = FL_METHOD_RESPONSE(fl_method_error_response_new( - "LinuxAudioError", - "Player has not yet been created or has already been disposed.", - nullptr)); - fl_method_call_respond(method_call, response, nullptr); - return; - } + auto player = audioplayers_linux_plugin_get_player(playerId); + if (!player) { + response = FL_METHOD_RESPONSE(fl_method_error_response_new( + "LinuxAudioError", + "Player has not yet been created or has already been disposed.", + nullptr)); + fl_method_call_respond(method_call, response, nullptr); + return; + } - FlValue* result = nullptr; + FlValue* result = nullptr; - try { if (strcmp(method, "pause") == 0) { player->Pause(); } else if (strcmp(method, "resume") == 0) { diff --git a/packages/audioplayers_platform_interface/test/audio_context_test.dart b/packages/audioplayers_platform_interface/test/audio_context_test.dart deleted file mode 100644 index f156d8634..000000000 --- a/packages/audioplayers_platform_interface/test/audio_context_test.dart +++ /dev/null @@ -1,148 +0,0 @@ -//ignore_for_file: avoid_redundant_argument_values - -import 'package:audioplayers_platform_interface/audioplayers_platform_interface.dart'; -import 'package:audioplayers_platform_interface/src/api/audio_context_config.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - test('Create default AudioContext', () async { - final context = AudioContext(); - expect( - context, - AudioContext( - android: const AudioContextAndroid( - isSpeakerphoneOn: false, - audioMode: AndroidAudioMode.normal, - stayAwake: false, - contentType: AndroidContentType.music, - usageType: AndroidUsageType.media, - audioFocus: AndroidAudioFocus.gain, - ), - iOS: AudioContextIOS( - category: AVAudioSessionCategory.playback, - options: const {}, - ), - ), - ); - }); - - test('Check AudioContextConfig assertions', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - const boolValues = {true, false}; - const focusValues = AudioContextConfigFocus.values; - const routeValues = AudioContextConfigRoute.values; - - final throwsAssertion = []; - for (final focus in focusValues) { - for (final isRespectSilence in boolValues) { - for (final isStayAwake in boolValues) { - for (final route in routeValues) { - final config = AudioContextConfig( - focus: focus, - respectSilence: isRespectSilence, - stayAwake: isStayAwake, - route: route, - ); - try { - config.build(); - throwsAssertion.add(false); - } on AssertionError catch (e) { - if (e.message - .toString() - .startsWith('Invalid AudioContextConfig')) { - throwsAssertion.add(true); - } else { - fail( - 'Assertion of $config does not match the expected ' - 'description. See: $e', - ); - } - } - } - } - } - } - - // Ensure assertions keep thrown on the correct cases. - expect( - throwsAssertion, - const [ - false, - false, - true, - false, - false, - true, - false, - false, - false, - false, - false, - false, - true, - true, - true, - true, - true, - true, - false, - false, - false, - false, - false, - false, - true, - true, - true, - true, - true, - true, - false, - false, - false, - false, - false, - false, - ], - ); - }); - - test('Create invalid AudioContextIOS', () async { - try { - // Throws AssertionError: - AudioContextIOS( - category: AVAudioSessionCategory.ambient, - options: const {AVAudioSessionOptions.mixWithOthers}, - ); - fail('AssertionError not thrown'); - // ignore: avoid_catches_without_on_clauses - } catch (e) { - expect(e, isInstanceOf()); - expect( - (e as AssertionError).message, - 'You can set the option `mixWithOthers` explicitly only if the audio ' - 'session category is `playAndRecord`, `playback`, or `multiRoute`.'); - } - }); - - test('Equality of AudioContextIOS', () async { - final context1 = AudioContextIOS( - category: AVAudioSessionCategory.playAndRecord, - options: const { - AVAudioSessionOptions.mixWithOthers, - AVAudioSessionOptions.defaultToSpeaker, - }, - ); - final context2 = AudioContextIOS( - category: AVAudioSessionCategory.playAndRecord, - options: const { - AVAudioSessionOptions.defaultToSpeaker, - AVAudioSessionOptions.mixWithOthers, - }, - ); - expect(context1, context2); - }); -} diff --git a/packages/audioplayers_platform_interface/test/audioplayers_platform_test.dart b/packages/audioplayers_platform_interface/test/audioplayers_platform_test.dart deleted file mode 100644 index 052d73b37..000000000 --- a/packages/audioplayers_platform_interface/test/audioplayers_platform_test.dart +++ /dev/null @@ -1,155 +0,0 @@ -import 'dart:async'; - -import 'package:audioplayers_platform_interface/src/api/audio_event.dart'; -import 'package:audioplayers_platform_interface/src/audioplayers_platform_interface.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'util.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final platform = AudioplayersPlatformInterface.instance; - - final methodCalls = []; - - void clear() { - methodCalls.clear(); - } - - MethodCall popCall() { - return methodCalls.removeAt(0); - } - - MethodCall popLastCall() { - expect(methodCalls, hasLength(1)); - return popCall(); - } - - group('AudioPlayers Method Channel', () { - setUp(() { - clear(); - - createNativeMethodHandler( - channel: 'xyz.luan/audioplayers', - handler: (MethodCall methodCall) async { - methodCalls.add(methodCall); - switch (methodCall.method) { - case 'getDuration': - return 0; - case 'getCurrentPosition': - return 0; - default: - return null; - } - }, - ); - }); - - test('#setSource', () async { - await platform.setSourceUrl( - 'p1', - 'internet.com/file.mp3', - mimeType: 'audio/wav', - ); - final call = popLastCall(); - expect(call.method, 'setSourceUrl'); - expect(call.args, { - 'playerId': 'p1', - 'url': 'internet.com/file.mp3', - 'isLocal': null, - 'mimeType': 'audio/wav', - }); - }); - - test('#resume', () async { - await platform.resume('p1'); - final call = popLastCall(); - expect(call.method, 'resume'); - expect(call.args, {'playerId': 'p1'}); - }); - - test('#pause', () async { - await platform.pause('p1'); - final call = popLastCall(); - expect(call.method, 'pause'); - expect(call.args, {'playerId': 'p1'}); - }); - - test('#getDuration', () async { - final duration = await platform.getDuration('p1'); - final call = popLastCall(); - expect(call.method, 'getDuration'); - expect(call.args, {'playerId': 'p1'}); - expect(duration, 0); - }); - - test('#getCurrentPosition', () async { - final position = await platform.getCurrentPosition('p1'); - final call = popLastCall(); - expect(call.method, 'getCurrentPosition'); - expect(call.args, {'playerId': 'p1'}); - expect(position, 0); - }); - }); - - group('AudioPlayers Event Channel', () { - test('emit events', () async { - final eventController = StreamController.broadcast(); - const playerId = 'p1'; - - createNativeEventStream( - channel: 'xyz.luan/audioplayers/events/$playerId', - byteDataStream: eventController.stream, - ); - - await platform.create(playerId); - - expect( - platform.getEventStream(playerId), - emitsInOrder([ - const AudioEvent( - eventType: AudioEventType.duration, - duration: Duration(milliseconds: 98765), - ), - const AudioEvent( - eventType: AudioEventType.log, - logMessage: 'someLogMessage', - ), - const AudioEvent( - eventType: AudioEventType.complete, - ), - const AudioEvent( - eventType: AudioEventType.seekComplete, - ), - ]), - ); - - final byteDataList = >[ - { - 'event': 'audio.onDuration', - 'value': 98765, - }, - { - 'event': 'audio.onLog', - 'value': 'someLogMessage', - }, - { - 'event': 'audio.onComplete', - }, - { - 'event': 'audio.onSeekComplete', - }, - ]; - for (final byteData in byteDataList) { - eventController.add( - const StandardMethodCodec().encodeSuccessEnvelope(byteData), - ); - } - - await eventController.close(); - await platform.dispose(playerId); - }); - }); -} diff --git a/packages/audioplayers_platform_interface/test/global_platform_test.dart b/packages/audioplayers_platform_interface/test/global_platform_test.dart deleted file mode 100644 index c0c588af8..000000000 --- a/packages/audioplayers_platform_interface/test/global_platform_test.dart +++ /dev/null @@ -1,126 +0,0 @@ -import 'dart:async'; - -import 'package:audioplayers_platform_interface/src/api/audio_context.dart'; -import 'package:audioplayers_platform_interface/src/api/global_audio_event.dart'; -import 'package:audioplayers_platform_interface/src/global_audioplayers_platform_interface.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'util.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final platform = GlobalAudioplayersPlatformInterface.instance; - - final methodCalls = []; - - void clear() { - methodCalls.clear(); - } - - MethodCall popCall() { - return methodCalls.removeAt(0); - } - - MethodCall popLastCall() { - expect(methodCalls, hasLength(1)); - return popCall(); - } - - group('Global Method Channel', () { - setUp(() { - clear(); - createNativeMethodHandler( - channel: 'xyz.luan/audioplayers.global', - handler: (MethodCall methodCall) async { - methodCalls.add(methodCall); - return null; - }, - ); - }); - - test('set AudioContext for Windows', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.windows; - await platform.setGlobalAudioContext(AudioContext()); - final call = popLastCall(); - expect(call.method, 'setAudioContext'); - expect(call.args, {}); - }); - - test('set AudioContext for macOS', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.macOS; - await platform.setGlobalAudioContext(AudioContext()); - final call = popLastCall(); - expect(call.method, 'setAudioContext'); - expect(call.args, {}); - }); - - test('set AudioContext for Linux', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.linux; - await platform.setGlobalAudioContext(AudioContext()); - final call = popLastCall(); - expect(call.method, 'setAudioContext'); - expect(call.args, {}); - }); - - test('set AudioContext for Android', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.android; - await platform.setGlobalAudioContext(AudioContext()); - final call = popLastCall(); - expect(call.method, 'setAudioContext'); - expect(call.args, { - 'isSpeakerphoneOn': false, - 'audioMode': 0, - 'stayAwake': false, - 'contentType': 2, - 'usageType': 1, - 'audioFocus': 1, - }); - }); - - test('set AudioContext for iOS', () async { - debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - await platform.setGlobalAudioContext(AudioContext()); - final call = popLastCall(); - expect(call.method, 'setAudioContext'); - expect(call.args, {'category': 'playback', 'options': []}); - }); - }); - - group('Global Event Channel', () { - test('emit global events', () async { - final eventController = StreamController.broadcast(); - - createNativeEventStream( - channel: 'xyz.luan/audioplayers.global/events', - byteDataStream: eventController.stream, - ); - - expect( - platform.getGlobalEventStream(), - emitsInOrder([ - const GlobalAudioEvent( - eventType: GlobalAudioEventType.log, - logMessage: 'someLogMessage', - ), - ]), - ); - - final byteDataList = >[ - { - 'event': 'audio.onLog', - 'value': 'someLogMessage', - }, - ]; - for (final byteData in byteDataList) { - eventController.add( - const StandardMethodCodec().encodeSuccessEnvelope(byteData), - ); - } - - await eventController.close(); - }); - }); -} diff --git a/packages/audioplayers_platform_interface/test/util.dart b/packages/audioplayers_platform_interface/test/util.dart deleted file mode 100644 index f3ac1ceb3..000000000 --- a/packages/audioplayers_platform_interface/test/util.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -extension MethodCallParser on MethodCall { - Map get args => arguments as Map; -} - -void createNativeMethodHandler({ - required String channel, - Future? Function(MethodCall message)? handler, -}) { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - MethodChannel(channel), - handler, - ); -} - -// See: https://github.com/flutter/packages/blob/12609a2abbb0a30b9d32af7b73599bfc834e609e/packages/video_player/video_player_android/test/android_video_player_test.dart#L270 -void createNativeEventStream({ - required String channel, - Stream? byteDataStream, -}) { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMessageHandler(channel, (ByteData? message) async { - final methodCall = const StandardMethodCodec().decodeMethodCall(message); - if (methodCall.method == 'listen') { - byteDataStream?.listen((byteData) async { - await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .handlePlatformMessage( - channel, - byteData, - (ByteData? data) {}, - ); - }); - return const StandardMethodCodec().encodeSuccessEnvelope(null); - } else if (methodCall.method == 'cancel') { - return const StandardMethodCodec().encodeSuccessEnvelope(null); - } else { - fail('Expected listen or cancel'); - } - }); -} diff --git a/packages/audioplayers_windows/windows/MediaEngineWrapper.cpp b/packages/audioplayers_windows/windows/MediaEngineWrapper.cpp index 8dacb2623..a467fef7f 100644 --- a/packages/audioplayers_windows/windows/MediaEngineWrapper.cpp +++ b/packages/audioplayers_windows/windows/MediaEngineWrapper.cpp @@ -19,6 +19,7 @@ #include #include "MediaEngineWrapper.h" #include "MediaFoundationHelpers.h" +#include "platform_thread_helper.h" #include "audioplayers_helpers.h" using namespace Microsoft::WRL; @@ -69,24 +70,54 @@ class MediaEngineCallbackHelper switch ((MF_MEDIA_ENGINE_EVENT)eventCode) { case MF_MEDIA_ENGINE_EVENT_LOADEDDATA: - m_onLoadedCB(); + if (m_onLoadedCB) { + auto callback = m_onLoadedCB; + PlatformThreadHelper::GetInstance().PostTask([callback]() { + callback(); + }); + } break; case MF_MEDIA_ENGINE_EVENT_ERROR: - m_errorCB((MF_MEDIA_ENGINE_ERR)param1, (HRESULT)param2); + if (m_errorCB) { + auto callback = m_errorCB; + auto err = (MF_MEDIA_ENGINE_ERR)param1; + auto hr = (HRESULT)param2; + PlatformThreadHelper::GetInstance().PostTask([callback, err, hr]() { + callback(err, hr); + }); + } break; case MF_MEDIA_ENGINE_EVENT_CANPLAY: - m_bufferingStateChangeCB( - MediaEngineWrapper::BufferingState::HAVE_ENOUGH); + if (m_bufferingStateChangeCB) { + auto callback = m_bufferingStateChangeCB; + PlatformThreadHelper::GetInstance().PostTask([callback]() { + callback(MediaEngineWrapper::BufferingState::HAVE_ENOUGH); + }); + } break; case MF_MEDIA_ENGINE_EVENT_WAITING: - m_bufferingStateChangeCB( - MediaEngineWrapper::BufferingState::HAVE_NOTHING); + if (m_bufferingStateChangeCB) { + auto callback = m_bufferingStateChangeCB; + PlatformThreadHelper::GetInstance().PostTask([callback]() { + callback(MediaEngineWrapper::BufferingState::HAVE_NOTHING); + }); + } break; case MF_MEDIA_ENGINE_EVENT_ENDED: - m_playbackEndedCB(); + if (m_playbackEndedCB) { + auto callback = m_playbackEndedCB; + PlatformThreadHelper::GetInstance().PostTask([callback]() { + callback(); + }); + } break; case MF_MEDIA_ENGINE_EVENT_SEEKED: - m_seekCompletedCB(); + if (m_seekCompletedCB) { + auto callback = m_seekCompletedCB; + PlatformThreadHelper::GetInstance().PostTask([callback]() { + callback(); + }); + } break; default: break; diff --git a/packages/audioplayers_windows/windows/audio_player.cpp b/packages/audioplayers_windows/windows/audio_player.cpp index 778b38775..a1773af83 100644 --- a/packages/audioplayers_windows/windows/audio_player.cpp +++ b/packages/audioplayers_windows/windows/audio_player.cpp @@ -21,9 +21,11 @@ using namespace winrt; AudioPlayer::AudioPlayer( std::string playerId, flutter::MethodChannel* methodChannel, + std::unique_ptr> eventChannel, EventStreamHandler<>* eventHandler) : _playerId(playerId), _methodChannel(methodChannel), + _eventChannel(std::move(eventChannel)), _eventHandler(eventHandler) { m_mfPlatform.Startup(); @@ -81,7 +83,7 @@ void AudioPlayer::SetSourceUrl(std::string url) { this->OnError("WindowsAudioError", "Failed to set source. For troubleshooting, " "see: " STR_LINK_TROUBLESHOOTING, - flutter::EncodableValue("Unknown Error setting url to '" + + flutter::EncodableValue("Unknown Error setting url to '" + url + "'.")); } } else { @@ -206,11 +208,13 @@ void AudioPlayer::OnSeekCompleted() { } void AudioPlayer::OnLog(const std::string& message) { - this->_eventHandler->Success(std::make_unique( - flutter::EncodableMap({{flutter::EncodableValue("event"), - flutter::EncodableValue("audio.onLog")}, - {flutter::EncodableValue("value"), - flutter::EncodableValue(message)}}))); + if (this->_eventHandler) { + this->_eventHandler->Success(std::make_unique( + flutter::EncodableMap({{flutter::EncodableValue("event"), + flutter::EncodableValue("audio.onLog")}, + {flutter::EncodableValue("value"), + flutter::EncodableValue(message)}}))); + } } void AudioPlayer::SendInitialized() { @@ -299,4 +303,4 @@ double AudioPlayer::GetDuration() { void AudioPlayer::SeekTo(double seek) { m_mediaEngineWrapper->SeekTo(seek); -} +} \ No newline at end of file diff --git a/packages/audioplayers_windows/windows/audio_player.h b/packages/audioplayers_windows/windows/audio_player.h index 3254f0454..feb54a0a4 100644 --- a/packages/audioplayers_windows/windows/audio_player.h +++ b/packages/audioplayers_windows/windows/audio_player.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include #include @@ -54,9 +54,12 @@ static std::unordered_map const releaseModeMap = { class AudioPlayer { public: - AudioPlayer(std::string playerId, - flutter::MethodChannel* methodChannel, - EventStreamHandler<>* eventHandler); + AudioPlayer( + std::string playerId, + flutter::MethodChannel* methodChannel, + std::unique_ptr> + eventChannel, + EventStreamHandler<>* eventHandler); void Dispose(); @@ -126,5 +129,7 @@ class AudioPlayer { flutter::MethodChannel* _methodChannel; + std::unique_ptr> _eventChannel; + EventStreamHandler<>* _eventHandler; }; diff --git a/packages/audioplayers_windows/windows/audioplayers_windows_plugin.cpp b/packages/audioplayers_windows/windows/audioplayers_windows_plugin.cpp index c84e8cbed..21679fef8 100644 --- a/packages/audioplayers_windows/windows/audioplayers_windows_plugin.cpp +++ b/packages/audioplayers_windows/windows/audioplayers_windows_plugin.cpp @@ -1,4 +1,4 @@ -#include "include/audioplayers_windows/audioplayers_windows_plugin.h" +#include "include/audioplayers_windows/audioplayers_windows_plugin.h" // This must be included before many other Windows headers. #include @@ -17,6 +17,7 @@ #include "audio_player.h" #include "audioplayers_helpers.h" +#include "platform_thread_helper.h" namespace { @@ -108,6 +109,9 @@ AudioplayersWindowsPlugin::~AudioplayersWindowsPlugin() {} void AudioplayersWindowsPlugin::HandleGlobalMethodCall( const MethodCall& method_call, std::unique_ptr> result) { + // Process pending events from background threads + PlatformThreadHelper::GetInstance().ProcessPendingTasks(); + auto args = method_call.arguments(); if (method_call.method_name().compare("init") == 0) { @@ -136,6 +140,9 @@ void AudioplayersWindowsPlugin::HandleGlobalMethodCall( void AudioplayersWindowsPlugin::HandleMethodCall( const MethodCall& method_call, std::unique_ptr> result) { + // IMPORTANT: Process pending events FIRST to deliver async callbacks + PlatformThreadHelper::GetInstance().ProcessPendingTasks(); + auto args = method_call.arguments(); auto playerId = GetArgument("playerId", args, std::string()); @@ -181,7 +188,9 @@ void AudioplayersWindowsPlugin::HandleMethodCall( return; } - std::thread(&AudioPlayer::SetSourceUrl, player, url).detach(); + // SetSourceUrl handles async loading internally via MediaFoundation + // Callbacks will fire asynchronously without needing std::thread + player->SetSourceUrl(url); } else if (method_call.method_name().compare("setSourceBytes") == 0) { auto data = GetArgument>("bytes", args, std::vector{}); @@ -192,7 +201,9 @@ void AudioplayersWindowsPlugin::HandleMethodCall( return; } - std::thread(&AudioPlayer::SetSourceBytes, player, data).detach(); + // SetSourceBytes handles async loading internally via MediaFoundation + // Callbacks will fire asynchronously without needing std::thread + player->SetSourceBytes(data); } else if (method_call.method_name().compare("getDuration") == 0) { auto duration = player->GetDuration(); result->Success(isnan(duration) @@ -251,6 +262,9 @@ void AudioplayersWindowsPlugin::HandleMethodCall( result->NotImplemented(); return; } + + // Process events again after the operation completes + PlatformThreadHelper::GetInstance().ProcessPendingTasks(); result->Success(); } @@ -266,8 +280,11 @@ void AudioplayersWindowsPlugin::CreatePlayer(std::string playerId) { eventChannel->SetStreamHandler(std::move(_ptr)); auto player = - std::make_unique(playerId, methods.get(), eventHandler); + std::make_unique(playerId, methods.get(), std::move(eventChannel), eventHandler); audioPlayers.insert(std::make_pair(playerId, std::move(player))); + + // Process any pending events from MediaEngine initialization + PlatformThreadHelper::GetInstance().ProcessPendingTasks(); } AudioPlayer* AudioplayersWindowsPlugin::GetPlayer(std::string playerId) { diff --git a/packages/audioplayers_windows/windows/event_stream_handler.h b/packages/audioplayers_windows/windows/event_stream_handler.h index 577f4c0ef..c98993806 100644 --- a/packages/audioplayers_windows/windows/event_stream_handler.h +++ b/packages/audioplayers_windows/windows/event_stream_handler.h @@ -1,6 +1,8 @@ #include #include +#include +#include #include using namespace flutter; @@ -14,16 +16,18 @@ class EventStreamHandler : public StreamHandler { void Success(std::unique_ptr _data) { std::unique_lock _ul(m_mtx); - if (m_sink.get()) - m_sink.get()->Success(*_data.get()); + if (m_sink) { + m_sink->Success(*_data); + } } void Error(const std::string& error_code, const std::string& error_message, const T& error_details) { std::unique_lock _ul(m_mtx); - if (m_sink.get()) - m_sink.get()->Error(error_code, error_message, error_details); + if (m_sink) { + m_sink->Error(error_code, error_message, error_details); + } } protected: diff --git a/packages/audioplayers_windows/windows/platform_thread_helper.h b/packages/audioplayers_windows/windows/platform_thread_helper.h new file mode 100644 index 000000000..09fb78145 --- /dev/null +++ b/packages/audioplayers_windows/windows/platform_thread_helper.h @@ -0,0 +1,51 @@ +#pragma once +#include +#include +#include +#include + +using namespace flutter; + +// Helper class to dispatch events to the platform thread +// Uses a thread-safe queue - events are processed on next platform thread call +class PlatformThreadHelper { + public: + static PlatformThreadHelper& GetInstance() { + static PlatformThreadHelper instance; + return instance; + } + + // Post an event to be executed on the platform thread + void PostTask(std::function task) { + std::lock_guard lock(m_mutex); + m_taskQueue.push(std::move(task)); + } + + // Process all pending tasks - should be called frequently from platform thread + void ProcessPendingTasks() { + std::queue> tasks; + { + std::lock_guard lock(m_mutex); + tasks.swap(m_taskQueue); + } + + while (!tasks.empty()) { + auto& task = tasks.front(); + try { + task(); + } catch (...) { + // Ignore exceptions to prevent crash + } + tasks.pop(); + } + } + + private: + PlatformThreadHelper() = default; + ~PlatformThreadHelper() = default; + PlatformThreadHelper(const PlatformThreadHelper&) = delete; + PlatformThreadHelper& operator=(const PlatformThreadHelper&) = delete; + + std::mutex m_mutex; + std::queue> m_taskQueue; +};