feat: Better in app video player

This commit is contained in:
Christian Pauly 2021-12-27 09:35:07 +01:00
parent 59b2e92328
commit e1cb8baf53
8 changed files with 145 additions and 199 deletions

View File

@ -2669,5 +2669,12 @@
"unsubscribeStories": "Unsubscribe stories",
"thisUserHasNotPostedAnythingYet": "This user has not posted anything in their story yet",
"yourStory": "Your story",
"replyHasBeenSent": "Reply has been sent"
"replyHasBeenSent": "Reply has been sent",
"videoWithSize": "Video ({size})",
"@videoWithSize": {
"type": "text",
"placeholders": {
"size": {}
}
}
}

View File

@ -4,8 +4,6 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/event_extension.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/string_color.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -92,13 +90,10 @@ class Message extends StatelessWidget {
bottomRight: const Radius.circular(AppConfig.borderRadius),
);
final noBubble = {
MessageTypes.Video,
MessageTypes.Image,
MessageTypes.Sticker,
}.contains(event.messageType) &&
!(event.messageType == MessageTypes.Video &&
!((PlatformInfos.isMobile && event.showThumbnail) ||
PlatformInfos.isWeb));
MessageTypes.Video,
MessageTypes.Image,
MessageTypes.Sticker,
}.contains(event.messageType);
if (ownMessage) {
color = displayEvent.status.isError

View File

@ -6,11 +6,10 @@ import 'package:matrix/matrix.dart';
import 'package:matrix_link_text/link_text.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/event_extension.dart';
import 'package:fluffychat/pages/chat/events/video_player.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../../config/app_config.dart';
import '../../../pages/video_viewer/video_viewer.dart';
import '../../../utils/platform_infos.dart';
import '../../../utils/url_launcher.dart';
import '../../bootstrap/bootstrap_dialog.dart';
@ -90,38 +89,7 @@ class MessageContent extends StatelessWidget {
return MessageDownloadContent(event, textColor);
case MessageTypes.Video:
if (PlatformInfos.isMobile || PlatformInfos.isWeb) {
if (event.showThumbnail) {
return Stack(
alignment: Alignment.center,
children: <Widget>[
ImageBubble(
event,
width: 400,
height: 300,
fit: BoxFit.cover,
tapToView: false,
),
FloatingActionButton.extended(
onPressed: () => showDialog(
context: Matrix.of(context).navigatorContext,
useRootNavigator: false,
builder: (_) => VideoViewer(event),
),
label: Text(L10n.of(context).play('Video')),
icon: const Icon(Icons.video_camera_front_outlined),
),
],
);
}
return FloatingActionButton.extended(
onPressed: () => showDialog(
context: Matrix.of(context).navigatorContext,
useRootNavigator: false,
builder: (_) => VideoViewer(event),
),
label: Text(L10n.of(context).play('Video')),
icon: const Icon(Icons.video_camera_front_outlined),
);
return EventVideoPlayer(event);
}
return MessageDownloadContent(event, textColor);
case MessageTypes.File:

View File

@ -0,0 +1,123 @@
//@dart=2.12
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flick_video_player/flick_video_player.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:path_provider/path_provider.dart';
import 'package:universal_html/html.dart' as html;
import 'package:video_player/video_player.dart';
import 'package:fluffychat/pages/chat/events/image_bubble.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/event_extension.dart';
import 'package:fluffychat/utils/sentry_controller.dart';
class EventVideoPlayer extends StatefulWidget {
final Event event;
const EventVideoPlayer(this.event, {Key? key}) : super(key: key);
@override
_EventVideoPlayerState createState() => _EventVideoPlayerState();
}
class _EventVideoPlayerState extends State<EventVideoPlayer> {
FlickManager? _flickManager;
bool _isDownloading = false;
String? _networkUri;
File? _tmpFile;
void _downloadAction() async {
setState(() => _isDownloading = true);
try {
final videoFile = await widget.event.downloadAndDecryptAttachment();
if (kIsWeb) {
final blob = html.Blob([videoFile.bytes]);
_networkUri = html.Url.createObjectUrlFromBlob(blob);
} else {
final tmpDir = await getTemporaryDirectory();
final file = File(tmpDir.path + videoFile.name);
if (await file.exists() == false) {
await file.writeAsBytes(videoFile.bytes);
}
_tmpFile = file;
}
} on MatrixConnectionException catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(e.toLocalizedString(context)),
));
} catch (e, s) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(e.toLocalizedString(context)),
));
SentryController.captureException(e, s);
} finally {
setState(() => _isDownloading = false);
}
}
@override
void dispose() {
_flickManager?.dispose();
super.dispose();
}
static const String fallbackBlurHash = 'L5H2EC=PM+yV0g-mq.wG9c010J}I';
@override
Widget build(BuildContext context) {
final hasThumbnail = widget.event.hasThumbnail;
final blurHash = (widget.event.infoMap as Map<String, dynamic>)
.tryGet<String>('xyz.amorgan.blurhash') ??
fallbackBlurHash;
final videoFile = _tmpFile;
final networkUri = _networkUri;
if (kIsWeb && networkUri != null && _flickManager == null) {
_flickManager = FlickManager(
videoPlayerController: VideoPlayerController.network(networkUri),
);
} else if (!kIsWeb && videoFile != null && _flickManager == null) {
_flickManager = FlickManager(
videoPlayerController: VideoPlayerController.file(videoFile),
autoPlay: true,
);
}
final flickManager = _flickManager;
return SizedBox(
width: 400,
height: 300,
child: Stack(
children: [
if (flickManager == null) ...[
if (hasThumbnail)
ImageBubble(widget.event)
else
BlurHash(hash: blurHash),
Center(
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.surface,
),
icon: _isDownloading
? const CircularProgressIndicator.adaptive(strokeWidth: 2)
: const Icon(Icons.download_outlined),
label: Text(
L10n.of(context)!
.videoWithSize(widget.event.sizeString ?? '?MB'),
),
onPressed: _isDownloading ? null : _downloadAction,
),
),
] else
FlickVideoPlayer(flickManager: flickManager),
],
),
);
}
}

View File

@ -1,94 +0,0 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:chewie/chewie.dart';
import 'package:matrix/matrix.dart';
import 'package:path_provider/path_provider.dart';
import 'package:video_player/video_player.dart';
import 'package:vrouter/vrouter.dart';
import '../../utils/matrix_sdk_extensions.dart/event_extension.dart';
import '../../utils/platform_infos.dart';
import '../../widgets/matrix.dart';
import 'video_viewer_view.dart';
class VideoViewer extends StatefulWidget {
final Event event;
const VideoViewer(this.event, {Key key}) : super(key: key);
@override
VideoViewerController createState() => VideoViewerController();
}
class VideoViewerController extends State<VideoViewer> {
VideoPlayerController videoPlayerController;
ChewieController chewieController;
dynamic error;
@override
void initState() {
super.initState();
(() async {
try {
if (widget.event.content['file'] is Map) {
if (PlatformInfos.isWeb) {
throw 'Encrypted videos unavailable in web';
}
final tempDirectory = (await getTemporaryDirectory()).path;
final mxcUri = widget.event.content
.tryGet<Map<String, dynamic>>('file')
?.tryGet<String>('url');
if (mxcUri == null) {
throw 'No mxc uri found';
}
// somehow the video viewer doesn't like the uri-encoded slashes, so we'll just gonna replace them with hyphons
final file = File(
'$tempDirectory/videos/${mxcUri.replaceAll(':', '').replaceAll('/', '-')}');
if (await file.exists() == false) {
final matrixFile =
await widget.event.downloadAndDecryptAttachmentCached();
await file.create(recursive: true);
await file.writeAsBytes(matrixFile.bytes);
}
videoPlayerController = VideoPlayerController.file(file);
} else if (widget.event.content['url'] is String) {
videoPlayerController = VideoPlayerController.network(
widget.event.getAttachmentUrl()?.toString());
} else {
throw 'invalid event';
}
await videoPlayerController.initialize();
chewieController = ChewieController(
videoPlayerController: videoPlayerController,
autoPlay: true,
looping: false,
);
setState(() => null);
} catch (e) {
setState(() => error = e);
}
})();
}
@override
void dispose() {
chewieController?.dispose();
videoPlayerController?.dispose();
super.dispose();
}
/// Forward this video to another room.
void forwardAction() {
Matrix.of(context).shareContent = widget.event.content;
VRouter.of(context).to('/rooms');
}
/// Save this file with a system call.
void saveFileAction() => widget.event.saveFile(context);
@override
Widget build(BuildContext context) => VideoViewerView(this);
}

View File

@ -1,53 +0,0 @@
import 'package:flutter/material.dart';
import 'package:chewie/chewie.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'video_viewer.dart';
class VideoViewerView extends StatelessWidget {
final VideoViewerController controller;
const VideoViewerView(this.controller, {Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
extendBodyBehindAppBar: true,
appBar: AppBar(
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: Navigator.of(context).pop,
color: Colors.white,
tooltip: L10n.of(context).close,
),
backgroundColor: const Color(0x44000000),
actions: [
IconButton(
icon: const Icon(Icons.reply_outlined),
onPressed: controller.forwardAction,
color: Colors.white,
tooltip: L10n.of(context).share,
),
IconButton(
icon: const Icon(Icons.download_outlined),
onPressed: controller.saveFileAction,
color: Colors.white,
tooltip: L10n.of(context).downloadFile,
),
],
),
body: Center(
child: controller.error != null
? Text(controller.error.toString())
: (controller.chewieController == null
? const CircularProgressIndicator.adaptive(strokeWidth: 2)
: Chewie(
controller: controller.chewieController,
)),
),
);
}
}

View File

@ -148,13 +148,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
chewie:
dependency: "direct main"
description:
name: chewie
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.2"
cli_util:
dependency: transitive
description:
@ -358,6 +351,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.2+1"
flick_video_player:
dependency: "direct main"
description:
name: flick_video_player
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.1"
fluffybox:
dependency: transitive
description:

View File

@ -13,7 +13,6 @@ dependencies:
audioplayers: ^0.20.1
blurhash_dart: ^1.1.0
cached_network_image: ^3.1.0
chewie: ^1.2.2
cupertino_icons: any
desktop_drop: ^0.2.0
desktop_notifications: ^0.6.1
@ -23,6 +22,7 @@ dependencies:
#fcm_shared_isolate:
# git: https://gitlab.com/famedly/libraries/fcm_shared_isolate.git
file_picker_cross: ^4.5.0
flick_video_player: ^0.3.1
flutter:
sdk: flutter
flutter_app_badger: ^1.3.0