Skip to content

Commit

Permalink
added video support for all platforms
Browse files Browse the repository at this point in the history
  • Loading branch information
clragon committed Aug 7, 2023
1 parent 434852a commit ad4cadd
Show file tree
Hide file tree
Showing 12 changed files with 538 additions and 428 deletions.
3 changes: 1 addition & 2 deletions lib/app/data/capabilities.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,5 @@ abstract final class PlatformCapabilities {

/// Whether this platform supports playing videos.
/// Platform views are not supported on desktop right now.
static bool get hasVideos =>
[Platform.isAndroid, Platform.isIOS].any((e) => e);
static bool get hasVideos => true;
}
8 changes: 2 additions & 6 deletions lib/app/widgets/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class App extends StatelessWidget {
drawerHeader: (context) => UserDrawerHeader(),
),
CurrentUserAvatarProvider(),
VideoServiceProvider(),
],
child: Consumer3<AppInfo, Settings, RouterDrawerController>(
builder: (context, appInfo, settings, navigation, child) =>
Expand Down Expand Up @@ -80,12 +81,7 @@ class App extends StatelessWidget {
child: ClientAvailabilityCheck(
child: AppLinkHandler(
child: NotificationHandler(
child: VideoHandlerData(
handler: VideoHandler(
muteVideos: settings.muteVideos.value,
),
child: child!,
),
child: child!,
),
),
),
Expand Down
165 changes: 74 additions & 91 deletions lib/interface/widgets/video.dart
Original file line number Diff line number Diff line change
@@ -1,121 +1,122 @@
import 'dart:math';
import 'dart:async';

import 'package:e1547/interface/interface.dart';
import 'package:e1547/logs/logs.dart';
import 'package:e1547/settings/settings.dart';
import 'package:flutter/material.dart';
import 'package:mutex/mutex.dart';
import 'package:video_player/video_player.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:media_kit/media_kit.dart';
import 'package:rxdart/rxdart.dart';

class VideoHandler extends ChangeNotifier {
VideoHandler({bool muteVideos = false}) : _muteVideos = muteVideos;
export 'package:media_kit_video/media_kit_video.dart';

static VideoHandler of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<VideoHandlerData>()!.handler;
class VideoPlayer extends Player {
VideoPlayer() {
_initialized = BehaviorSubject.seeded(false);
controller.waitUntilFirstFrameRendered.then((_) => _initialized.add(true));
}

late final VideoController _controller = VideoController(this);
VideoController get controller => _controller;

late final BehaviorSubject<bool> _initialized;
Stream<bool> get initialized => _initialized.stream;

bool get isInitialized => _initialized.value;
}

class VideoService extends ChangeNotifier {
VideoService({bool muteVideos = false}) : _muteVideos = muteVideos;

static void ensureInitialized() => MediaKit.ensureInitialized();

// To prevent the app from crashing due tue OutOfMemoryErrors,
// the list of all loaded videos is global.
static final Map<VideoConfig, VideoPlayerController> _videos = {};
static final Map<VideoConfig, VideoPlayer> _videos = {};

final Loggy _loggy = Loggy('Videos');

final int maxLoaded = 3;

// 50mb
final int maxSize = 5 * pow(10, 7).toInt();

final Mutex _loadingLock = Mutex();

bool _muteVideos;

bool get muteVideos => _muteVideos;

set muteVideos(bool value) {
_muteVideos = value;
_videos.values.forEach((e) => e.setVolume(muteVideos ? 0 : 1));
_videos.values.forEach((e) => e.setVolume(muteVideos ? 0 : 100));
notifyListeners();
_loggy.debug('${_muteVideos ? 'Muted' : 'Unmuted'} all controllers');
}

VideoPlayerController getVideo(VideoConfig key) => _videos.putIfAbsent(
key,
() => VideoPlayerController.networkUrl(
Uri.parse(key.url),
videoPlayerOptions: VideoPlayerOptions(
mixWithOthers: true,
),
),
);

Future<void> loadVideo(VideoConfig key) async =>
_loadingLock.protect(() async {
VideoPlayerController? controller = getVideo(key);
if (controller.value.isInitialized) return;

while (true) {
Map<VideoConfig, VideoPlayerController> loaded = Map.of(_videos)
..removeWhere((key, value) => !value.value.isInitialized);
int loadedSize = loaded.keys
.fold<int>(0, (current, config) => current + config.size);
if (loaded.length < maxLoaded && loadedSize < maxSize) {
break;
}
_loggy.debug(
'Too many (${loaded.length}) or too large ($loadedSize) videos loaded!');
await disposeVideo(loaded.keys.first);
}

controller.addListener(controller.wakelock);
await controller.setLooping(true);
await controller.setVolume(muteVideos ? 0 : 1);
await controller.initialize();
notifyListeners();
_loggy.debug('Loaded $key');
});
VideoPlayer getVideo(VideoConfig key) {
while (true) {
Map<VideoConfig, VideoPlayer> loaded = Map.of(_videos);
loaded.remove(key);
if (loaded.length < maxLoaded) break;
_loggy.debug('Too many (${loaded.length}) videos loaded!');
disposeVideo(loaded.keys.first);
}
return _videos.putIfAbsent(
key,
() {
VideoPlayer player = VideoPlayer();
player.open(Media(key.url), play: false);
player.setPlaylistMode(PlaylistMode.single);
player.setVolume(_muteVideos ? 0 : 100);
return player;
},
);
}

Future<void> disposeVideo(VideoConfig key) async {
VideoPlayerController? controller = _videos[key];
VideoPlayer? controller = _videos[key];
if (controller != null) {
_videos.remove(key);
await controller.pause();
controller.removeListener(controller.wakelock);
await controller.dispose();
_videos.remove(key);
notifyListeners();
_loggy.debug('Unloaded $key');
}
}
}

class VideoHandlerData extends InheritedNotifier<VideoHandler> {
const VideoHandlerData({required this.handler, required super.child})
: super(notifier: handler);
@immutable
class VideoConfig {
const VideoConfig({required this.url, required this.size});

final VideoHandler handler;
final String url;
final int size;

@override
bool updateShouldNotify(covariant VideoHandlerData oldWidget) =>
oldWidget.handler != handler;
}

extension Wake on VideoPlayerController {
void wakelock() {
value.isPlaying ? WakelockPlus.enable() : WakelockPlus.disable();
}
}
bool operator ==(Object other) =>
other is VideoConfig && other.url == url && other.size == size;

class VideoHandlerVolumeControl extends StatefulWidget {
const VideoHandlerVolumeControl();
@override
int get hashCode => Object.hash(url.hashCode, size.hashCode);

@override
State<VideoHandlerVolumeControl> createState() =>
_VideoHandlerVolumeControlState();
String toString() => 'Video($url)';
}

class VideoServiceProvider
extends SubChangeNotifierProvider<Settings, VideoService> {
VideoServiceProvider({super.child, super.builder})
: super(
create: (context, settings) => VideoService(
muteVideos: settings.muteVideos.value,
),
);
}

class _VideoHandlerVolumeControlState extends State<VideoHandlerVolumeControl> {
class VideoServiceVolumeControl extends StatelessWidget {
const VideoServiceVolumeControl();

@override
Widget build(BuildContext context) {
bool muted = VideoHandler.of(context).muteVideos;
VideoService service = context.watch<VideoService>();
bool muted = service.muteVideos;
return InkWell(
onTap: () => VideoHandler.of(context).muteVideos = !muted,
onTap: () => service.muteVideos = !muted,
child: Padding(
padding: const EdgeInsets.all(4),
child: Icon(
Expand All @@ -127,21 +128,3 @@ class _VideoHandlerVolumeControlState extends State<VideoHandlerVolumeControl> {
);
}
}

@immutable
class VideoConfig {
const VideoConfig({required this.url, required this.size});

final String url;
final int size;

@override
bool operator ==(Object other) =>
other is VideoConfig && other.url == url && other.size == size;

@override
int get hashCode => Object.hash(url.hashCode, size.hashCode);

@override
String toString() => 'Video($url)';
}
1 change: 1 addition & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:flutter/services.dart';

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
VideoService.ensureInitialized();
AppInfo appInfo = await initializeAppInfo();
AppDatabases databases = await initializeAppdatabases(info: appInfo);
Logs logs = await initializeLogger(databases: databases);
Expand Down
12 changes: 6 additions & 6 deletions lib/post/data/actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import 'package:e1547/interface/interface.dart';
import 'package:e1547/post/post.dart';
import 'package:e1547/tag/tag.dart';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';

extension PostTagging on Post {
bool hasTag(String tag) {
Expand Down Expand Up @@ -159,22 +158,23 @@ extension PostVideoPlaying on Post {
)
: null;

VideoPlayerController? getVideo(BuildContext context) {
VideoPlayer? getVideo(BuildContext context) {
if (videoConfig != null) {
return VideoHandler.of(context).getVideo(videoConfig!);
return context.read<VideoService>().getVideo(videoConfig!);
}
return null;
}

Future<void> loadVideo(BuildContext context) async {
VideoPlayer? watchVideo(BuildContext context) {
if (videoConfig != null) {
await VideoHandler.of(context).loadVideo(videoConfig!);
return context.watch<VideoService>().getVideo(videoConfig!);
}
return null;
}

Future<void> disposeVideo(BuildContext context) async {
if (videoConfig != null) {
await VideoHandler.of(context).disposeVideo(videoConfig!);
await context.read<VideoService>().disposeVideo(videoConfig!);
}
}
}
Expand Down
61 changes: 26 additions & 35 deletions lib/post/widgets/detail/image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'package:e1547/app/app.dart';
import 'package:e1547/interface/interface.dart';
import 'package:e1547/post/post.dart';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';

class PostDetailImage extends StatelessWidget {
const PostDetailImage({required this.post});
Expand All @@ -27,37 +26,31 @@ class PostDetailVideo extends StatelessWidget {

@override
Widget build(BuildContext context) {
VideoPlayerController? videoController = post.getVideo(context);
return PostVideoLoader(
post: post,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: videoController != null
? () => videoController.value.isPlaying
? videoController.pause()
: videoController.play()
: null,
child: Stack(
alignment: Alignment.center,
fit: StackFit.passthrough,
children: [
PostVideoWidget(post: post),
Positioned.fill(
child: Center(
child: CrossFade.builder(
showChild: post.getVideo(context) != null,
builder: (context) => Padding(
padding:
const EdgeInsets.symmetric(vertical: 16, horizontal: 8),
child:
VideoButton(videoController: post.getVideo(context)!),
),
secondChild: const SizedCircularProgressIndicator(size: 24),
VideoPlayer? player = post.getVideo(context);
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: player != null
? () => player.state.playing ? player.pause() : player.play()
: null,
child: Stack(
alignment: Alignment.center,
fit: StackFit.passthrough,
children: [
PostVideoWidget(post: post),
Positioned.fill(
child: Center(
child: CrossFade.builder(
showChild: post.getVideo(context) != null,
builder: (context) => Padding(
padding:
const EdgeInsets.symmetric(vertical: 16, horizontal: 8),
child: VideoButton(player: post.getVideo(context)!),
),
secondChild: const SizedCircularProgressIndicator(size: 24),
),
),
],
),
),
],
),
);
}
Expand Down Expand Up @@ -176,22 +169,20 @@ class PostDetailImageActions extends StatelessWidget {
builder: (context) => const Card(
elevation: 0,
color: Colors.black12,
child: VideoHandlerVolumeControl(),
child: VideoServiceVolumeControl(),
),
);
}

VideoPlayerController? videoController = post.getVideo(context);
VideoPlayer? player = post.getVideo(context);

return Stack(
fit: StackFit.passthrough,
children: [
InkWell(
hoverColor: Colors.transparent,
onTap: videoController != null
? () => videoController.value.isPlaying
? videoController.pause()
: videoController.play()
onTap: player != null
? () => player.state.playing ? player.pause() : player.play()
: onTap,
child: child,
),
Expand Down
Loading

0 comments on commit ad4cadd

Please sign in to comment.