import 'dart:typed_data'; import 'package:apidash/consts.dart'; import 'package:apidash/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:just_audio/just_audio.dart'; typedef AudioErrorWidgetBuilder = Widget Function( BuildContext context, Object error, StackTrace? stackTrace, ); // Uint8List AudioSource for just_audio class Uint8AudioSource extends StreamAudioSource { Uint8AudioSource(this.bytes, {required this.contentType}); final List bytes; final String contentType; @override Future request([int? start, int? end]) async { start ??= 0; end ??= bytes.length; return StreamAudioResponse( sourceLength: bytes.length, contentLength: end - start, offset: start, stream: Stream.value(bytes.sublist(start, end)), contentType: contentType, ); } } class Uint8AudioPlayer extends StatefulWidget { /// Creates a widget for playing audio obtained from a [Uint8List]. const Uint8AudioPlayer({ super.key, required this.bytes, required this.type, required this.subtype, required this.errorBuilder, }); final Uint8List bytes; final String type; final String subtype; final AudioErrorWidgetBuilder errorBuilder; @override State createState() => _Uint8AudioPlayerState(); } class _Uint8AudioPlayerState extends State { final player = AudioPlayer(); @override void initState() { player.setAudioSource(Uint8AudioSource( widget.bytes, contentType: '${widget.type}/${widget.subtype}', )); super.initState(); } @override void dispose() { player.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return StreamBuilder( stream: player.playerStateStream, builder: (context, snapshot) { if (snapshot.hasError) { return widget.errorBuilder( context, snapshot.error!, snapshot.stackTrace); } else { final playerState = snapshot.data; final processingState = playerState?.processingState; if (processingState == ProcessingState.ready || processingState == ProcessingState.completed || processingState == ProcessingState.buffering) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Audio Player Padding( padding: kPh20v10, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // Duration Position Builder (time elapsed) _buildDuration( player.positionStream, maxDuration: player.duration, ), // Slider to view & change Duration Position _buildPositionBar( player.positionStream, maxDuration: player.duration, onChanged: (value) => player.seek(Duration(seconds: value.toInt())), ), // Total Duration Text( audioPosition(player.duration), style: TextStyle(fontFamily: kCodeStyle.fontFamily), ), ], ), ), // Audio Player Controls Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ // Play/Pause Button _buildPlayButton( player.playingStream, play: player.play, pause: player.pause, restart: () => player.seek(Duration.zero), completed: processingState == ProcessingState.completed, ), // Mute/UnMute button _buildVolumeButton( player.volumeStream, mute: () => player.setVolume(0), unmute: () => player.setVolume(1), ), ], ), ], ); } else if (processingState == ProcessingState.idle) { // Error in Loading AudioSource return widget.errorBuilder( context, ErrorDescription('${player.audioSource} Loading Error'), snapshot.stackTrace, ); } else { return const Center(child: CircularProgressIndicator()); } } }, ); } StreamBuilder _buildPlayButton( Stream stream, { VoidCallback? play, VoidCallback? pause, VoidCallback? restart, required bool completed, }) { return StreamBuilder( stream: stream, builder: (context, snapshot) { final playing = snapshot.data; if (playing != true) { return IconButton( icon: const Icon(Icons.play_arrow), onPressed: play, ); } else if (completed) { return IconButton( icon: const Icon(Icons.replay), onPressed: restart, ); } else { return IconButton( icon: const Icon(Icons.pause), onPressed: pause, ); } }, ); } StreamBuilder _buildDuration( Stream stream, { Duration? maxDuration, }) { return StreamBuilder( stream: stream, builder: (context, snapshot) { final position = snapshot.data; return Text( audioPosition(position), style: TextStyle(fontFamily: kCodeStyle.fontFamily), ); }, ); } StreamBuilder _buildPositionBar( Stream stream, { Duration? maxDuration, ValueChanged? onChanged, }) { return StreamBuilder( stream: stream, builder: (context, snapshot) { return Flexible( child: SliderTheme( data: SliderTheme.of(context).copyWith( trackShape: const RectangularSliderTrackShape(), thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8.0), overlayShape: const RoundSliderOverlayShape(overlayRadius: 16.0), ), child: Slider( value: snapshot.data?.inSeconds.toDouble() ?? 0, max: maxDuration?.inSeconds.toDouble() ?? 0, onChanged: onChanged, ), ), ); }, ); } StreamBuilder _buildVolumeButton(Stream stream, {VoidCallback? mute, VoidCallback? unmute}) { return StreamBuilder( stream: stream, builder: (context, snapshot) { return snapshot.data == 0 ? IconButton(icon: const Icon(Icons.volume_off), onPressed: unmute) : IconButton(icon: const Icon(Icons.volume_up), onPressed: mute); }, ); } }