Skip to content

Commit 03fb937

Browse files
content: Implement inline video preview
1 parent de239ea commit 03fb937

File tree

4 files changed

+423
-12
lines changed

4 files changed

+423
-12
lines changed

lib/widgets/content.dart

Lines changed: 140 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import 'package:flutter/foundation.dart';
22
import 'package:flutter/gestures.dart';
33
import 'package:flutter/material.dart';
4+
import 'package:flutter/scheduler.dart';
45
import 'package:flutter/services.dart';
56
import 'package:html/dom.dart' as dom;
67
import 'package:intl/intl.dart';
78
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
9+
import 'package:video_player/video_player.dart';
810

911
import '../api/core.dart';
1012
import '../api/model/model.dart';
13+
import '../log.dart';
1114
import '../model/avatar_url.dart';
1215
import '../model/binding.dart';
1316
import '../model/content.dart';
@@ -99,13 +102,7 @@ class BlockContentList extends StatelessWidget {
99102
);
100103
return MessageImage(node: node);
101104
} else if (node is InlineVideoNode) {
102-
return Text.rich(
103-
TextSpan(children: [
104-
const TextSpan(text: "(unimplemented:", style: errorStyle),
105-
TextSpan(text: node.debugHtmlText, style: errorCodeStyle),
106-
const TextSpan(text: ")", style: errorStyle),
107-
]),
108-
style: errorStyle);
105+
return MessageInlineVideo(node: node);
109106
} else if (node is EmbedVideoNode) {
110107
return MessageEmbedVideo(node: node);
111108
} else if (node is UnimplementedBlockContentNode) {
@@ -410,6 +407,142 @@ class MessageImage extends StatelessWidget {
410407
}
411408
}
412409

410+
class MessageInlineVideo extends StatefulWidget {
411+
const MessageInlineVideo({super.key, required this.node});
412+
413+
final InlineVideoNode node;
414+
415+
@override
416+
State<MessageInlineVideo> createState() => _MessageInlineVideoState();
417+
}
418+
419+
class _MessageInlineVideoState extends State<MessageInlineVideo> {
420+
Uri? _resolvedSrcUrl;
421+
422+
VideoPlayerController? _controller;
423+
bool _didAttemptInitialization = false;
424+
bool _hasNonPlatformError = false;
425+
426+
@override
427+
void initState() {
428+
// We delay initialization by a single frame to make sure the
429+
// BuildContext is a valid context as its needed to retrieve
430+
// the PerAccountStore during initialization.
431+
SchedulerBinding.instance.addPostFrameCallback((_) => _initialize());
432+
super.initState();
433+
}
434+
435+
Future<void> _initialize() async {
436+
try {
437+
final store = PerAccountStoreWidget.of(context);
438+
// _resolvedSrcUrl = store.tryResolveUrl(widget.node.srcUrl);
439+
if (_resolvedSrcUrl == null) {
440+
return; // TODO(log)
441+
}
442+
443+
assert(debugLog('VideoPlayerController.networkUrl($_resolvedSrcUrl)'));
444+
_controller = VideoPlayerController.networkUrl(_resolvedSrcUrl!, httpHeaders: {
445+
if (_resolvedSrcUrl!.origin == store.account.realmUrl.origin) ...authHeader(
446+
email: store.account.email,
447+
apiKey: store.account.apiKey,
448+
),
449+
...userAgentHeader()
450+
});
451+
452+
await _controller!.initialize();
453+
_controller!.addListener(_handleVideoControllerUpdates);
454+
} on PlatformException catch (error) {
455+
// This can occur when the video url
456+
// We silently fail here, because we handle this initialization error
457+
// by opening the video 'srcUrl' externally.
458+
assert(debugLog("VideoPlayerController.initialize failed: $error"));
459+
} catch (error) {
460+
_hasNonPlatformError = true;
461+
assert(debugLog("VideoPlayerController.initialize failed: $error"));
462+
} finally {
463+
if (mounted) setState(() { _didAttemptInitialization = true; });
464+
}
465+
}
466+
467+
@override
468+
void dispose() {
469+
_controller?.removeListener(_handleVideoControllerUpdates);
470+
_controller?.dispose();
471+
super.dispose();
472+
}
473+
474+
void _handleVideoControllerUpdates() {
475+
assert(debugLog("Video buffered: ${_controller?.value.buffered}"));
476+
assert(debugLog("Video max duration: ${_controller?.value.duration}"));
477+
}
478+
479+
@override
480+
Widget build(BuildContext context) {
481+
final message = InheritedMessage.of(context);
482+
483+
return GestureDetector(
484+
onTap: !_didAttemptInitialization
485+
? null
486+
: () { // TODO(log)
487+
if (_resolvedSrcUrl == null || _hasNonPlatformError) {
488+
showErrorDialog(context: context,
489+
title: 'Unable to open video',
490+
message: 'Video could not be opened: $_resolvedSrcUrl');
491+
return;
492+
}
493+
494+
if (_controller!.value.hasError) {
495+
// TODO use webview instead, to support auth headers
496+
_launchUrl(context, widget.node.srcUrl);
497+
} else {
498+
Navigator.of(context).push(getLightboxRoute(
499+
context: context,
500+
message: message,
501+
src: _resolvedSrcUrl!,
502+
videoController: _controller,
503+
));
504+
}
505+
},
506+
child: UnconstrainedBox(
507+
alignment: Alignment.centerLeft,
508+
child: Padding(
509+
// TODO clean up this padding by imitating web less precisely;
510+
// in particular, avoid adding loose whitespace at end of message.
511+
padding: const EdgeInsets.only(right: 5, bottom: 5),
512+
child: Container(
513+
height: 100,
514+
width: 150,
515+
color: Colors.black,
516+
child: !_didAttemptInitialization
517+
? Container(
518+
color: Colors.black,
519+
child: const Center(
520+
child: SizedBox(
521+
height: 16,
522+
width: 16,
523+
child: CircularProgressIndicator(
524+
color: Colors.white,
525+
strokeWidth: 2))))
526+
: Stack(
527+
alignment: Alignment.center,
528+
children: [
529+
if (_resolvedSrcUrl == null || _controller!.value.hasError)
530+
Container(color: Colors.black)
531+
else
532+
LightboxHero(
533+
message: message,
534+
src: _resolvedSrcUrl!,
535+
child: AspectRatio(
536+
aspectRatio: _controller!.value.aspectRatio,
537+
child: VideoPlayer(_controller!))),
538+
const Icon(
539+
Icons.play_arrow_rounded,
540+
color: Colors.white,
541+
size: 32)
542+
])))));
543+
}
544+
}
545+
413546
/// MessageEmbedVideo opens the video href externally, and
414547
/// a preview image is visible in the content message UI.
415548
class MessageEmbedVideo extends StatelessWidget {

0 commit comments

Comments
 (0)