Flutter音频播放插件simple_audio_fork的使用

Flutter音频播放插件simple_audio_fork的使用

Simple Audio Fork 是一个用于在 Flutter 中播放音频的跨平台解决方案。它旨在提供简单且稳定的 API,支持多种功能,包括本地和在线资源的播放、无间隙播放、音量规范化等。虽然这是一个分叉版本,但其功能与原始插件基本一致。

特性

  • 简单的 API
  • 跨平台支持(Android、Linux、Windows、iOS、macOS)
  • 媒体控制器支持
    • Linux: MPRIS
    • Android: MediaSessionCompat
    • Windows: SystemMediaTransportControls
    • iOS/macOS: 控制中心
  • 支持本地和在线资源播放
  • 无间隙播放和预加载
  • 音量规范化

文档

该插件的文档托管在 pub.dev 上,可以在以下链接找到:simple_audio 文档


使用方法

1. 添加依赖

首先,在 pubspec.yaml 文件中添加 simple_audio 作为依赖:

dependencies:
  simple_audio: ^最新版本号

然后运行 flutter pub get 来安装依赖。


2. 初始化插件

在应用启动时调用 SimpleAudio.init() 方法进行初始化。以下是初始化的示例代码:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 初始化插件,默认值
  await SimpleAudio.init(
    useMediaController: true, // 是否启用媒体控制器
    shouldNormalizeVolume: false, // 是否启用音量规范化
    dbusName: "com.erikas.SimpleAudio", // D-Bus 名称
    actions: [
      MediaControlAction.rewind, // 倒退
      MediaControlAction.skipPrev, // 上一首
      MediaControlAction.playPause, // 播放/暂停
      MediaControlAction.skipNext, // 下一首
      MediaControlAction.fastForward, // 快进
    ],
    androidNotificationIconPath: "mipmap/ic_launcher", // Android 通知图标路径
    androidCompactActions: [1, 2, 3], // Android 通知按钮
    applePreferSkipButtons: true, // iOS 是否优先使用跳过按钮
  );

  runApp(const MyApp());
}

3. 创建播放器实例

在需要播放音频的地方创建 SimpleAudio 实例,并监听事件。以下是创建播放器的示例代码:

class _MyAppState extends State<MyApp> {
  final SimpleAudio player = SimpleAudio(
    onSkipNext: (_) => debugPrint("Next"), // 上一首回调
    onSkipPrevious: (_) => debugPrint("Prev"), // 下一首回调
    onNetworkStreamError: (player, error) { // 网络流错误回调
      debugPrint("Network Stream Error: $error");
      player.stop();
    },
    onDecodeError: (player, error) { // 解码错误回调
      debugPrint("Decode Error: $error");
      player.stop();
    },
  );

  PlaybackState playbackState = PlaybackState.stop; // 当前播放状态
  bool get isPlaying => playbackState == PlaybackState.play || playbackState == PlaybackState.preloadPlayed; // 是否正在播放
  bool get isMuted => volume == 0; // 是否静音
  double trueVolume = 1; // 实际音量
  double volume = 1; // 当前音量
  bool normalize = false; // 是否启用音量规范化
  bool loop = false; // 是否循环播放

  double position = 0; // 当前播放位置
  double duration = 0; // 音频总时长

  // 将秒数转换为可读格式
  String convertSecondsToReadableString(int seconds) {
    int m = seconds ~/ 60;
    int s = seconds % 60;
    return "$m:${s > 9 ? s : "0$s"}";
  }

  // 选择文件
  Future<String> pickFile() async {
    FilePickerResult? file = await FilePicker.platform.pickFiles(
      dialogTitle: "Pick file to play.",
      type: FileType.audio,
    );

    final PlatformFile pickedFile = file!.files.single;
    return pickedFile.path!;
  }

  [@override](/user/override)
  void initState() {
    super.initState();

    // 监听播放状态变化
    player.playbackStateStream.listen((event) {
      setState(() => playbackState = event);
    });

    // 监听播放进度变化
    player.progressStateStream.listen((event) {
      setState(() {
        position = event.position.toDouble();
        duration = event.duration.toDouble();
      });
    });
  }
}

4. 操作播放器

通过调用 SimpleAudio 提供的方法来控制音频播放。以下是一些常用的播放操作示例:

ElevatedButton(
  child: const Text("Open File"),
  onPressed: () async {
    String path = await pickFile();

    // 设置元数据
    await player.setMetadata(
      const Metadata(
        title: "Title",
        artist: "Artist",
        album: "Album",
        artUri: "https://picsum.photos/200",
      ),
    );

    // 停止当前播放并打开新文件
    await player.stop();
    await player.open(path);
  },
),

5. 播放预加载的文件

除了直接播放文件外,还可以预加载音频文件以便后续播放:

ElevatedButton(
  child: const Text("Preload File"),
  onPressed: () async {
    String path = await pickFile();
    await player.preload(path); // 预加载文件
  },
),

ElevatedButton(
  child: const Text("Play Preload"),
  onPressed: () async {
    if (!await player.hasPreloaded) {
      debugPrint("No preloaded file to play!");
      return;
    }

    debugPrint("Playing preloaded file.");
    await player.stop();
    await player.playPreload(); // 播放预加载文件
  },
),

ElevatedButton(
  child: const Text("Clear Preload"),
  onPressed: () async {
    if (!await player.hasPreloaded) {
      debugPrint("No preloaded file to clear!");
      return;
    }

    debugPrint("Cleared preloaded file.");
    await player.clearPreload(); // 清除预加载文件
  },
),

6. 控制播放状态

可以通过按钮或滑块控制播放状态,例如停止、播放/暂停、调整音量等:

Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    CircleButton(
      size: 35,
      onPressed: playbackState != PlaybackState.done ? player.stop : null,
      child: const Icon(Icons.stop, color: Colors.white),
    ),
    const SizedBox(width: 10),
    CircleButton(
      size: 40,
      onPressed: () {
        if (isPlaying) {
          player.pause();
        } else {
          player.play();
        }
      },
      child: Icon(
        isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded,
        color: Colors.white,
      ),
    ),
    const SizedBox(width: 10),
    CircleButton(
      size: 35,
      onPressed: () {
        if (!isMuted) {
          player.setVolume(0);
          setState(() => volume = 0);
        } else {
          player.setVolume(trueVolume);
          setState(() => volume = trueVolume);
        }
      },
      child: Icon(
        isMuted ? Icons.volume_off : Icons.volume_up,
        color: Colors.white,
      ),
    ),
  ],
),

平台特定设置

Windows

无需额外设置。

Linux

linux/my_application.cc 文件中添加以下代码以支持 MPRIS:

static void my_application_activate(GApplication* application) {
  GList* windows = gtk_application_get_windows(GTK_APPLICATION(application));
  if(windows) {
    gtk_window_present_with_time(
      GTK_WINDOW(windows->data),
      g_get_monotonic_time() / 1000
    );
    return;
  }

  MyApplication* self = MY_APPLICATION(application);
  // ...
}

MyApplication* my_application_new() {
  return MY_APPLICATION(g_object_new(my_application_get_type(),
    "application-id", APPLICATION_ID, "flags",
    G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN,
    nullptr
  ));
}

Android

编辑 android/app/src/main/AndroidManifest.xml 文件,添加以下内容:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

<service
    android:name="com.erikas.simple_audio.SimpleAudioService"
    android:foregroundServiceType="mediaPlayback"
    android:exported="false">
    <intent-filter>
        <action android:name="android.media.browse.MediaBrowserService" />
    </intent-filter>
</service>

<receiver
    android:name="com.erikas.simple_audio.SimpleAudioReceiver" />

iOS

在 Xcode 中添加 AudioToolbox.framework,并在 Info.plist 中添加以下内容:

<key>UIBackgroundModes</key>
<array>
  <string>audio</string>
</array>

macOS

将 macOS 的最低部署版本更新为 10.13

  1. 打开 macos/Runner.xcworkspace
  2. 在项目设置中将 macOS Deployment TargetMinimum Deployments macOS 版本设置为 10.13

完整示例代码

以下是一个完整的示例代码,展示了如何使用 Simple Audio Fork 插件播放音频:

import 'dart:io';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:file_picker/file_picker.dart';
import 'package:simple_audio/simple_audio.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 初始化插件
  await SimpleAudio.init(
    useMediaController: true,
    shouldNormalizeVolume: false,
    dbusName: "com.erikas.SimpleAudio",
    actions: [
      MediaControlAction.rewind,
      MediaControlAction.skipPrev,
      MediaControlAction.playPause,
      MediaControlAction.skipNext,
      MediaControlAction.fastForward,
    ],
    androidNotificationIconPath: "mipmap/ic_launcher",
    androidCompactActions: [1, 2, 3],
    applePreferSkipButtons: true,
  );

  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  [@override](/user/override)
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final SimpleAudio player = SimpleAudio(
    onSkipNext: (_) => debugPrint("Next"),
    onSkipPrevious: (_) => debugPrint("Prev"),
    onNetworkStreamError: (player, error) {
      debugPrint("Network Stream Error: $error");
      player.stop();
    },
    onDecodeError: (player, error) {
      debugPrint("Decode Error: $error");
      player.stop();
    },
  );

  PlaybackState playbackState = PlaybackState.stop;
  bool get isPlaying => playbackState == PlaybackState.play || playbackState == PlaybackState.preloadPlayed;
  bool get isMuted => volume == 0;
  double trueVolume = 1;
  double volume = 1;
  bool normalize = false;
  bool loop = false;

  double position = 0;
  double duration = 0;

  String convertSecondsToReadableString(int seconds) {
    int m = seconds ~/ 60;
    int s = seconds % 60;
    return "$m:${s > 9 ? s : "0$s"}";
  }

  Future<String> pickFile() async {
    FilePickerResult? file = await FilePicker.platform.pickFiles(
      dialogTitle: "Pick file to play.",
      type: FileType.audio,
    );

    final PlatformFile pickedFile = file!.files.single;
    return pickedFile.path!;
  }

  [@override](/user/override)
  void initState() {
    super.initState();

    player.playbackStateStream.listen((event) {
      setState(() => playbackState = event);
    });

    player.progressStateStream.listen((event) {
      setState(() {
        position = event.position.toDouble();
        duration = event.duration.toDouble();
      });
    });
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Simple Audio Example'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              if (Platform.isAndroid || Platform.isIOS) ...{
                Builder(
                  builder: (context) => ElevatedButton(
                    child: const Text("Get Storage Perms"),
                    onPressed: () async {
                      PermissionStatus status = await Permission.storage.request();

                      if (!mounted) return;
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(
                          content: Text("Storage Permissions: ${status.name}"),
                        ),
                      );
                    },
                  ),
                ),
              },
              const SizedBox(height: 5),
              ElevatedButton(
                child: const Text("Open File"),
                onPressed: () async {
                  String path = await pickFile();

                  await player.setMetadata(
                    const Metadata(
                      title: "Title",
                      artist: "Artist",
                      album: "Album",
                      artUri: "https://picsum.photos/200",
                    ),
                  );
                  await player.stop();
                  await player.open(path);
                },
              ),
              const SizedBox(height: 10),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  ElevatedButton(
                    child: const Text("Preload File"),
                    onPressed: () async {
                      String path = await pickFile();
                      await player.preload(path);
                    },
                  ),
                  const SizedBox(width: 5),
                  ElevatedButton(
                    child: const Text("Play Preload"),
                    onPressed: () async {
                      if (!await player.hasPreloaded) {
                        debugPrint("No preloaded file to play!");
                        return;
                      }

                      debugPrint("Playing preloaded file.");
                      await player.stop();
                      await player.playPreload();
                    },
                  ),
                  const SizedBox(width: 5),
                  ElevatedButton(
                    child: const Text("Clear Preload"),
                    onPressed: () async {
                      if (!await player.hasPreloaded) {
                        debugPrint("No preloaded file to clear!");
                        return;
                      }

                      debugPrint("Cleared preloaded file.");
                      await player.clearPreload();
                    },
                  ),
                ],
              ),
              const SizedBox(height: 20),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  CircleButton(
                    size: 35,
                    onPressed: playbackState != PlaybackState.done ? player.stop : null,
                    child: const Icon(Icons.stop, color: Colors.white),
                  ),
                  const SizedBox(width: 10),
                  CircleButton(
                    size: 40,
                    onPressed: () {
                      if (isPlaying) {
                        player.pause();
                      } else {
                        player.play();
                      }
                    },
                    child: Icon(
                      isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded,
                      color: Colors.white,
                    ),
                  ),
                  const SizedBox(width: 10),
                  CircleButton(
                    size: 35,
                    onPressed: () {
                      if (!isMuted) {
                        player.setVolume(0);
                        setState(() => volume = 0);
                      } else {
                        player.setVolume(trueVolume);
                        setState(() => volume = trueVolume);
                      }
                    },
                    child: Icon(
                      isMuted ? Icons.volume_off : Icons.volume_up,
                      color: Colors.white,
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 10),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Text("Volume: "),
                  SizedBox(
                    width: 200,
                    child: Slider(
                      value: volume,
                      onChanged: (value) {
                        setState(() {
                          volume = value;
                          trueVolume = value;
                        });
                        player.setVolume(value);
                      },
                    ),
                  ),
                ],
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Checkbox(
                    value: loop,
                    onChanged: (value) {
                      setState(() => loop = value!);
                      player.loopPlayback(loop);
                    },
                  ),
                  const Text("Loop Playback"),
                ],
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Checkbox(
                    value: normalize,
                    onChanged: (value) {
                      setState(() => normalize = value!);
                      player.normalizeVolume(normalize);
                    },
                  ),
                  const Text("Normalize Volume"),
                ],
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 10),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Text(convertSecondsToReadableString(position.floor())),
                    Flexible(
                      child: ConstrainedBox(
                        constraints: const BoxConstraints(maxWidth: 450),
                        child: Slider(
                          value: min(position, duration),
                          max: duration,
                          onChanged: (value) {
                            player.seek(value.floor());
                          },
                        ),
                      ),
                    ),
                    Text(convertSecondsToReadableString(duration.floor())),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class CircleButton extends StatelessWidget {
  const CircleButton({
    required this.onPressed,
    required this.child,
    this.size = 35,
    this.color = Colors.blue,
    super.key,
  });

  final void Function()? onPressed;
  final Widget child;
  final double size;
  final Color color;

  [@override](/user/override)
  Widget build(BuildContext context) {
    return SizedBox(
      height: size,
      width: size,
      child: ClipOval(
        child: Material(
          color: color,
          child: InkWell(
            canRequestFocus: false,
            onTap: onPressed,
            child: child,
          ),
        ),
      ),
    );
  }
}

更多关于Flutter音频播放插件simple_audio_fork的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter音频播放插件simple_audio_fork的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


simple_audio_fork 是一个 Flutter 插件,用于在 Flutter 应用中播放音频。它是 simple_audio 的一个分支版本,提供了简单的 API 来播放、暂停、停止和控制音频播放。以下是如何在 Flutter 项目中使用 simple_audio_fork 的基本步骤。

1. 添加依赖

首先,在 pubspec.yaml 文件中添加 simple_audio_fork 依赖:

dependencies:
  flutter:
    sdk: flutter
  simple_audio_fork: ^1.0.0  # 请检查最新版本

然后运行 flutter pub get 来安装依赖。

2. 导入插件

在需要使用音频播放功能的 Dart 文件中导入 simple_audio_fork

import 'package:simple_audio_fork/simple_audio_fork.dart';

3. 初始化音频播放器

在使用音频播放器之前,需要先初始化它:

final audioPlayer = SimpleAudio();

4. 播放音频

使用 play 方法来播放音频。你可以传递本地文件路径或网络 URL:

await audioPlayer.play('assets/audio/sample.mp3');  // 播放本地文件
// 或者
await audioPlayer.play('https://example.com/sample.mp3');  // 播放网络音频

5. 控制播放

你可以使用以下方法来控制音频播放:

  • 暂停播放:

    await audioPlayer.pause();
    
  • 继续播放:

    await audioPlayer.resume();
    
  • 停止播放:

    await audioPlayer.stop();
    
  • 跳转到指定位置:

    await audioPlayer.seek(Duration(seconds: 30));  // 跳转到30秒处
    

6. 监听播放状态

你可以监听音频播放的状态,例如播放进度、播放完成等:

audioPlayer.onPlayerStateChanged.listen((state) {
  print('Player state: $state');
});

audioPlayer.onPositionChanged.listen((position) {
  print('Current position: $position');
});

audioPlayer.onDurationChanged.listen((duration) {
  print('Total duration: $duration');
});

7. 释放资源

在不再需要音频播放器时,释放资源:

await audioPlayer.dispose();

8. 处理错误

你可以监听错误事件来处理播放过程中可能出现的错误:

audioPlayer.onPlayerError.listen((error) {
  print('Player error: $error');
});

完整示例

以下是一个完整的示例,展示了如何使用 simple_audio_fork 播放音频:

import 'package:flutter/material.dart';
import 'package:simple_audio_fork/simple_audio_fork.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      home: AudioPlayerScreen(),
    );
  }
}

class AudioPlayerScreen extends StatefulWidget {
  [@override](/user/override)
  _AudioPlayerScreenState createState() => _AudioPlayerScreenState();
}

class _AudioPlayerScreenState extends State<AudioPlayerScreen> {
  final audioPlayer = SimpleAudio();

  [@override](/user/override)
  void initState() {
    super.initState();
    audioPlayer.onPlayerStateChanged.listen((state) {
      print('Player state: $state');
    });
  }

  [@override](/user/override)
  void dispose() {
    audioPlayer.dispose();
    super.dispose();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Simple Audio Fork Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () async {
                await audioPlayer.play('assets/audio/sample.mp3');
              },
              child: Text('Play'),
            ),
            ElevatedButton(
              onPressed: () async {
                await audioPlayer.pause();
              },
              child: Text('Pause'),
            ),
            ElevatedButton(
              onPressed: () async {
                await audioPlayer.resume();
              },
              child: Text('Resume'),
            ),
            ElevatedButton(
              onPressed: () async {
                await audioPlayer.stop();
              },
              child: Text('Stop'),
            ),
          ],
        ),
      ),
    );
  }
}
回到顶部