Feat: Audio Player

This commit is contained in:
Dushant
2023-06-01 20:52:14 +05:30
parent 736fd89276
commit b578b302b7
7 changed files with 293 additions and 1 deletions

View File

@ -78,6 +78,7 @@ const kTabHeight = 45.0;
const kHeaderHeight = 32.0;
const kSegmentHeight = 24.0;
const kTextButtonMinWidth = 36.0;
const kSliderWidth = 160.0;
const kRandMax = 100000;
@ -424,6 +425,9 @@ const kUnexpectedRaiseIssue =
const kImageError =
"There seems to be an issue rendering this image. Please raise an issue in API Dash GitHub repo so that we can resolve it.";
const kAudioError =
"There seems to be an issue playing this audio. Please raise an issue in API Dash GitHub repo so that we can resolve it.";
const kRaiseIssue =
"\nPlease raise an issue in API Dash GitHub repo so that we can resolve it.";

View File

@ -24,6 +24,14 @@ String humanizeDuration(Duration? duration) {
}
}
String audioPosition(Duration? duration) {
if (duration == null) return "";
var min = duration.inMinutes;
var secs = duration.inSeconds.remainder(60);
var secondsPadding = secs < 10 ? "0" : "";
return "$min:$secondsPadding$secs";
}
String capitalizeFirstLetter(String? text) {
if (text == null || text == "") {
return "";

View File

@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'error_message.dart';
import 'package:apidash/consts.dart';
import 'uint8_audio_player.dart';
class Previewer extends StatefulWidget {
const Previewer({
@ -36,7 +37,14 @@ class _PreviewerState extends State<Previewer> {
// TODO: PDF Viewer
}
if (widget.type == kTypeAudio) {
// TODO: Audio Player
return Uint8AudioPlayer(
bytes: widget.bytes,
type: widget.type,
subtype: widget.subtype,
errorBuilder: (context, _, stacktrace) {
return const ErrorMessage(message: kAudioError);
},
);
}
if (widget.type == kTypeVideo) {
// TODO: Video Player

View File

@ -0,0 +1,230 @@
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, {this.type = 'audio', this.subtype = 'mpeg'});
final List<int> bytes;
final String? type;
final String? subtype;
@override
Future<StreamAudioResponse> 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: '$type/$subtype',
);
}
}
class Uint8AudioPlayer extends StatefulWidget {
/// Creates a widget for playing audio obtained from a [Uint8List].
const Uint8AudioPlayer({
super.key,
required this.bytes,
this.type,
this.subtype,
required this.errorBuilder,
});
final Uint8List bytes;
final String? type;
final String? subtype;
final AudioErrorWidgetBuilder errorBuilder;
@override
State<Uint8AudioPlayer> createState() => _Uint8AudioPlayerState();
}
class _Uint8AudioPlayerState extends State<Uint8AudioPlayer> {
final player = AudioPlayer();
@override
void initState() {
player.setAudioSource(Uint8AudioSource(widget.bytes,
type: widget.type, subtype: widget.subtype));
super.initState();
}
@override
void dispose() {
player.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return StreamBuilder<PlayerState>(
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
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Duration Position Builder (time elapsed / total duration)
_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 {
return const Center(child: CircularProgressIndicator());
}
}
},
);
}
StreamBuilder<bool> _buildPlayButton(
Stream<bool> stream, {
VoidCallback? play,
VoidCallback? pause,
VoidCallback? restart,
required bool completed,
}) {
return StreamBuilder<bool>(
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<Duration> _buildDuration(
Stream<Duration> stream, {
Duration? maxDuration,
}) {
return StreamBuilder<Duration>(
stream: stream,
builder: (context, snapshot) {
final position = snapshot.data;
return Text(
audioPosition(position),
style: TextStyle(fontFamily: kCodeStyle.fontFamily),
);
},
);
}
StreamBuilder<Duration> _buildPositionBar(
Stream<Duration> stream, {
Duration? maxDuration,
ValueChanged<double>? onChanged,
}) {
return StreamBuilder<Duration>(
stream: stream,
builder: (context, snapshot) {
return SizedBox(
width: kSliderWidth,
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<double> _buildVolumeButton(Stream<double> stream,
{VoidCallback? mute, VoidCallback? unmute}) {
return StreamBuilder<double>(
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);
},
);
}
}

View File

@ -16,3 +16,4 @@ export 'request_widgets.dart';
export 'response_widgets.dart';
export 'snackbars.dart';
export 'markdown.dart';
export 'uint8_audio_player.dart';

View File

@ -41,6 +41,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.10.0"
audio_session:
dependency: transitive
description:
name: audio_session
sha256: "655343841a723646f74932215c5785ef7156b76d2de4b99bcd3205476f08dc61"
url: "https://pub.dev"
source: hosted
version: "0.1.15"
axis_layout:
dependency: transitive
description:
@ -360,6 +368,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.6.1"
just_audio:
dependency: "direct main"
description:
name: just_audio
sha256: "890cd0fc41a1a4530c171e375a2a3fb6a09d84e9d508c5195f40bcff54330327"
url: "https://pub.dev"
source: hosted
version: "0.9.34"
just_audio_platform_interface:
dependency: transitive
description:
name: just_audio_platform_interface
sha256: d8409da198bbc59426cd45d4c92fca522a2ec269b576ce29459d6d6fcaeb44df
url: "https://pub.dev"
source: hosted
version: "4.2.1"
just_audio_web:
dependency: transitive
description:
name: just_audio_web
sha256: ff62f733f437b25a0ff590f0e295fa5441dcb465f1edbdb33b3dea264705bc13
url: "https://pub.dev"
source: hosted
version: "0.4.8"
lints:
dependency: transitive
description:
@ -584,6 +616,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.4"
rxdart:
dependency: transitive
description:
name: rxdart
sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
url: "https://pub.dev"
source: hosted
version: "0.27.7"
screen_retriever:
dependency: transitive
description:

View File

@ -33,6 +33,7 @@ dependencies:
path: ^1.8.2
flutter_markdown: ^0.6.14
markdown: ^7.1.0
just_audio: ^0.9.34
dev_dependencies:
flutter_test: