feat: Better in app video player
							parent
							
								
									7e5259bb4b
								
							
						
					
					
						commit
						8a371d53b9
					
				@ -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),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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);
 | 
			
		||||
}
 | 
			
		||||
@ -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,
 | 
			
		||||
                  )),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
					Loading…
					
					
				
		Reference in New Issue