From c7abac6a328bdbf0c21bdfee6b2a1d0dff3a26eb Mon Sep 17 00:00:00 2001 From: tanay Date: Sat, 23 Jan 2021 18:06:26 -0500 Subject: [PATCH 1/5] Fix audio issue on iOS - remove chewie_audio and replace with assets_audio_player --- example/lib/main.dart | 7 +- lib/src/replaced_element.dart | 14 +- lib/src/utils.dart | 43 +++ lib/src/widgets/custom_audio_widget.dart | 460 +++++++++++++++++++++++ pubspec.yaml | 2 +- 5 files changed, 510 insertions(+), 16 deletions(-) create mode 100644 lib/src/widgets/custom_audio_widget.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index eda1888c5c..db4c407895 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -35,6 +35,9 @@ const htmlData = """

Header 4

Header 5
Header 6
+

Ruby Support:

@@ -128,9 +131,7 @@ const htmlData = """

Audio support:

- +

IFrame support:

"""; diff --git a/lib/src/replaced_element.dart b/lib/src/replaced_element.dart index 565c1a8b55..918d1ac38c 100644 --- a/lib/src/replaced_element.dart +++ b/lib/src/replaced_element.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'dart:math'; import 'package:chewie/chewie.dart'; -import 'package:chewie_audio/chewie_audio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -11,6 +10,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_html/html_parser.dart'; import 'package:flutter_html/src/html_elements.dart'; import 'package:flutter_html/src/utils.dart'; +import 'package:flutter_html/src/widgets/custom_audio_widget.dart'; import 'package:flutter_html/style.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:html/dom.dart' as dom; @@ -253,17 +253,7 @@ class AudioContentElement extends ReplacedElement { Widget toWidget(RenderContext context) { return Container( width: context.style.width ?? 300, - child: ChewieAudio( - controller: ChewieAudioController( - videoPlayerController: VideoPlayerController.network( - src.first ?? "", - ), - autoPlay: autoplay, - looping: loop, - showControls: showControls, - autoInitialize: true, - ), - ), + child: CustomAudioWidget(showControls: showControls, src: src, autoplay: autoplay, loop: loop, context: context, muted: muted) ); } } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index fddb87d88e..98a53cc21b 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -43,3 +43,46 @@ class MultipleTapGestureRecognizer extends TapGestureRecognizer { } } } + +String getMMSSFormat(Duration d) { + String twoDigits(int n) { + if (n >= 10) return "$n"; + return "0$n"; + } + + String twoDigitMinutes = twoDigits(d.inMinutes.remainder(Duration.minutesPerHour)); + String twoDigitSeconds = twoDigits(d.inSeconds.remainder(Duration.secondsPerMinute)); + return "$twoDigitMinutes:$twoDigitSeconds"; +} + +String formatDuration(Duration position) { + final ms = position.inMilliseconds; + + int seconds = ms ~/ 1000; + final int hours = seconds ~/ 3600; + seconds = seconds % 3600; + final minutes = seconds ~/ 60; + seconds = seconds % 60; + + final hoursString = hours >= 10 + ? '$hours' + : hours == 0 + ? '00' + : '0$hours'; + + final minutesString = minutes >= 10 + ? '$minutes' + : minutes == 0 + ? '00' + : '0$minutes'; + + final secondsString = seconds >= 10 + ? '$seconds' + : seconds == 0 + ? '00' + : '0$seconds'; + + final formattedTime = '${hoursString == '00' ? '' : '$hoursString:'}$minutesString:$secondsString'; + + return formattedTime; +} \ No newline at end of file diff --git a/lib/src/widgets/custom_audio_widget.dart b/lib/src/widgets/custom_audio_widget.dart new file mode 100644 index 0000000000..20b097c4cf --- /dev/null +++ b/lib/src/widgets/custom_audio_widget.dart @@ -0,0 +1,460 @@ +import 'dart:io'; +import 'dart:ui'; + +import 'package:assets_audio_player/assets_audio_player.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_html/html_parser.dart'; +import 'package:flutter_html/src/utils.dart'; + +class CustomAudioWidget extends StatefulWidget { + final List src; + final bool showControls; + final bool autoplay; + final bool loop; + final bool muted; + final RenderContext context; + + CustomAudioWidget({ + @required this.src, + @required this.showControls, + @required this.autoplay, + @required this.loop, + @required this.muted, + @required this.context, + }); + + @override + State createState() { + return CustomAudioWidgetState(); + } +} + +class CustomAudioWidgetState extends State with SingleTickerProviderStateMixin { + final assetsAudioPlayer = AssetsAudioPlayer(); + bool wasPlaying; + + @override + initState() { + if (widget.src.first != null) { + assetsAudioPlayer.open(Audio.network(widget.src.first), autoStart: widget.autoplay ?? false, showNotification: true); + assetsAudioPlayer.setLoopMode(widget.loop == true ? LoopMode.single : LoopMode.none); + assetsAudioPlayer.setVolume(0.5); + } + super.initState(); + } + + @override + dispose() { + assetsAudioPlayer.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext buildContext) { + if (widget.src.first != null) { + return assetsAudioPlayer.builderRealtimePlayingInfos( + builder: (BuildContext context, info) { + if (info == null) { + return AspectRatio(aspectRatio: 1, child: CircularProgressIndicator()); + } else if (Platform.isAndroid) { + return Container( + height: 48, + color: Theme.of(buildContext).dialogBackgroundColor, + child: Row( + children: [ + GestureDetector( + onTap: () { + if (info.isPlaying) { + assetsAudioPlayer.pause(); + } else { + assetsAudioPlayer.play(); + } + }, + child: Container( + height: 48, + color: Colors.transparent, + margin: const EdgeInsets.only(left: 8.0, right: 4.0), + padding: const EdgeInsets.only( + left: 12.0, + right: 12.0, + ), + child: Icon( + info.isPlaying ? Icons.pause : Icons.play_arrow, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(right: 24.0), + child: Text( + "${getMMSSFormat(info.currentPosition)} / ${getMMSSFormat(info.duration)}", + style: const TextStyle( + fontSize: 14.0, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 20.0), + child: GestureDetector( + onHorizontalDragStart: (DragStartDetails details) { + wasPlaying = info.isPlaying; + if (info.isPlaying) { + assetsAudioPlayer.pause(); + } + }, + onHorizontalDragUpdate: (DragUpdateDetails details) { + final box = context.findRenderObject() as RenderBox; + final Offset tapPos = box.globalToLocal(details.globalPosition); + final double relative = tapPos.dx / box.size.width; + final Duration position = info.duration * relative; + assetsAudioPlayer.seek(position); + }, + onHorizontalDragEnd: (DragEndDetails details) { + if (wasPlaying) { + assetsAudioPlayer.play(); + } + }, + onTapDown: (TapDownDetails details) { + final box = context.findRenderObject() as RenderBox; + final Offset tapPos = box.globalToLocal(details.globalPosition); + final double relative = tapPos.dx / box.size.width; + final Duration position = info.duration * relative; + assetsAudioPlayer.seek(position); + }, + child: Center( + child: Container( + height: MediaQuery.of(context).size.height / 2, + width: MediaQuery.of(context).size.width, + color: Colors.transparent, + child: CustomPaint( + painter: MaterialProgressBarPainter( + info, + ProgressColors(playedColor: Theme.of(context).primaryColor, handleColor: Theme.of(context).primaryColor), + ), + ), + ), + ), + ), + ), + ), + GestureDetector( + onTap: () { + if (info.volume == 0) { + assetsAudioPlayer.setVolume(0.5); + } else { + assetsAudioPlayer.setVolume(0); + } + }, + child: ClipRect( + child: Container( + height: 48, + padding: const EdgeInsets.only( + left: 8.0, + right: 8.0, + ), + child: Icon( + info.volume == 0 ? Icons.volume_off : Icons.volume_up, + ), + ), + ), + ) + ], + ), + ); + } else { + final barHeight = MediaQuery.of(context).orientation == Orientation.portrait ? 30.0 : 47.0; + double latestVolume; + return Container( + color: Colors.transparent, + alignment: Alignment.center, + margin: EdgeInsets.all(5.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(10.0), + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 10.0, + sigmaY: 10.0, + ), + child: Container( + height: barHeight, + color: Color.fromRGBO(41, 41, 41, 0.7), + child: Row( + children: [ + GestureDetector( + onTap: () { + assetsAudioPlayer.seekBy(Duration(seconds: -15)); + }, + child: Container( + height: barHeight, + color: Colors.transparent, + margin: const EdgeInsets.only(left: 10.0), + padding: const EdgeInsets.only( + left: 6.0, + right: 6.0, + ), + child: Icon( + CupertinoIcons.gobackward_15, + color: Color.fromARGB(255, 200, 200, 200), + size: 18.0, + ), + ), + ), + GestureDetector( + onTap: () { + if (info.isPlaying) { + assetsAudioPlayer.pause(); + } else { + assetsAudioPlayer.play(); + } + }, + child: Container( + height: barHeight, + color: Colors.transparent, + padding: const EdgeInsets.only( + left: 6.0, + right: 6.0, + ), + child: Icon( + info.isPlaying ? Icons.pause : Icons.play_arrow, + color: Color.fromARGB(255, 200, 200, 200), + ), + ), + ), + GestureDetector( + onTap: () { + assetsAudioPlayer.seekBy(Duration(seconds: 15)); + }, + child: Container( + height: barHeight, + color: Colors.transparent, + padding: const EdgeInsets.only( + left: 6.0, + right: 8.0, + ), + margin: const EdgeInsets.only( + right: 8.0, + ), + child: Icon( + CupertinoIcons.goforward_15, + color: Color.fromARGB(255, 200, 200, 200), + size: 18.0, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(right: 12.0), + child: Text( + formatDuration(info.currentPosition), + style: TextStyle( + color: Color.fromARGB(255, 200, 200, 200), + fontSize: 12.0, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 12.0), + child: GestureDetector( + onHorizontalDragStart: (DragStartDetails details) { + wasPlaying = info.isPlaying; + if (info.isPlaying) { + assetsAudioPlayer.pause(); + } + }, + onHorizontalDragUpdate: (DragUpdateDetails details) { + final box = context.findRenderObject() as RenderBox; + final Offset tapPos = box.globalToLocal(details.globalPosition); + final double relative = tapPos.dx / box.size.width; + final Duration position = info.duration * relative; + assetsAudioPlayer.seek(position); + }, + onHorizontalDragEnd: (DragEndDetails details) { + if (wasPlaying) { + assetsAudioPlayer.play(); + } + }, + onTapDown: (TapDownDetails details) { + final box = context.findRenderObject() as RenderBox; + final Offset tapPos = box.globalToLocal(details.globalPosition); + final double relative = tapPos.dx / box.size.width; + final Duration position = info.duration * relative; + assetsAudioPlayer.seek(position); + }, + child: Center( + child: Container( + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + color: Colors.transparent, + child: CustomPaint( + painter: CupertinoProgressBarPainter( + info, + ProgressColors( + playedColor: const Color.fromARGB(120, 255, 255, 255,), + handleColor: const Color.fromARGB(255, 255, 255, 255,), + bufferedColor: const Color.fromARGB(60, 255, 255, 255,), + backgroundColor: const Color.fromARGB(20, 255, 255, 255,), + ), + ), + ), + ), + ), + ) + ), + ), + Padding( + padding: const EdgeInsets.only(right: 12.0), + child: Text( + '-${formatDuration(info.duration - info.currentPosition)}', + style: TextStyle(color: Color.fromARGB(255, 200, 200, 200), fontSize: 12.0), + ), + ), + Padding( + padding: const EdgeInsets.only(right: 12), + child: GestureDetector( + onTap: () { + if (info.volume == 0) { + assetsAudioPlayer.setVolume(latestVolume ?? 0.5); + } else { + latestVolume = info.volume; + assetsAudioPlayer.setVolume(0.0); + } + }, + child: SizedBox( + height: barHeight, + child: Icon( + info.volume > 0 ? Icons.volume_up : Icons.volume_off, + color: Color.fromARGB(255, 200, 200, 200), + size: 16.0, + ), + ), + ), + ) + ], + ), + ), + ), + ), + ); + } + } + ); + } else { + return Container(height: 0, width: 0); + } + } +} + +class MaterialProgressBarPainter extends CustomPainter { + MaterialProgressBarPainter(this.info, this.colors); + + RealtimePlayingInfos info; + ProgressColors colors; + + @override + bool shouldRepaint(CustomPainter painter) { + return true; + } + + @override + void paint(Canvas canvas, Size size) { + const height = 2.0; + + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromPoints( + Offset(0.0, size.height / 2), + Offset(size.width, size.height / 2 + height), + ), + const Radius.circular(4.0), + ), + colors.backgroundPaint, + ); + final double playedPartPercent = info.duration.inMilliseconds == 0 ? 0 : info.currentPosition.inMilliseconds / info.duration.inMilliseconds; + final double playedPart = playedPartPercent >= 1 ? size.width : playedPartPercent * size.width; + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromPoints( + Offset(0.0, size.height / 2), + Offset(playedPart, size.height / 2 + height), + ), + const Radius.circular(4.0), + ), + colors.playedPaint, + ); + canvas.drawCircle( + Offset(playedPart, size.height / 2 + height / 2), + height * 3, + colors.handlePaint, + ); + } +} + +class CupertinoProgressBarPainter extends CustomPainter { + CupertinoProgressBarPainter(this.info, this.colors); + + RealtimePlayingInfos info; + ProgressColors colors; + + @override + bool shouldRepaint(CustomPainter painter) { + return true; + } + + @override + void paint(Canvas canvas, Size size) { + const barHeight = 5.0; + const handleHeight = 6.0; + final baseOffset = size.height / 2 - barHeight / 2.0; + + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromPoints( + Offset(0.0, baseOffset), + Offset(size.width, baseOffset + barHeight), + ), + const Radius.circular(4.0), + ), + colors.backgroundPaint, + ); + final double playedPartPercent = info.duration.inMilliseconds == 0 ? 0 : info.currentPosition.inMilliseconds / info.duration.inMilliseconds; + final double playedPart = playedPartPercent > 1 ? size.width : playedPartPercent * size.width; + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromPoints( + Offset(0.0, baseOffset), + Offset(playedPart, baseOffset + barHeight), + ), + const Radius.circular(4.0), + ), + colors.playedPaint, + ); + + final shadowPath = Path() + ..addOval(Rect.fromCircle(center: Offset(playedPart, baseOffset + barHeight / 2), radius: handleHeight)); + + canvas.drawShadow(shadowPath, Colors.black, 0.2, false); + canvas.drawCircle( + Offset(playedPart, baseOffset + barHeight / 2), + handleHeight, + colors.handlePaint, + ); + } +} + +class ProgressColors { + ProgressColors({ + Color playedColor = const Color.fromRGBO(255, 0, 0, 0.7), + Color bufferedColor = const Color.fromRGBO(30, 30, 200, 0.2), + Color handleColor = const Color.fromRGBO(200, 200, 200, 1.0), + Color backgroundColor = const Color.fromRGBO(200, 200, 200, 0.5), + }) : playedPaint = Paint()..color = playedColor, + bufferedPaint = Paint()..color = bufferedColor, + handlePaint = Paint()..color = handleColor, + backgroundPaint = Paint()..color = backgroundColor; + + final Paint playedPaint; + final Paint bufferedPaint; + final Paint handlePaint; + final Paint backgroundPaint; +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 0a94a6b93d..f5847d5afd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: webview_flutter: ^1.0.0 # Plugins for rendering the