mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-03-11 17:25:32 +00:00
parent
623a29c39c
commit
5fca41dc58
238 changed files with 48 additions and 35550 deletions
|
|
@ -5,7 +5,6 @@ import 'package:flex_color_scheme/flex_color_scheme.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:fvp/fvp.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
|
@ -32,14 +31,7 @@ void main(List<String> args) async {
|
|||
return;
|
||||
}
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
registerWith(options: {
|
||||
'platforms': ['windows']
|
||||
});
|
||||
} else {
|
||||
MediaKit.ensureInitialized();
|
||||
}
|
||||
|
||||
MediaKit.ensureInitialized();
|
||||
await RustLib.init();
|
||||
if (!(Platform.isAndroid || Platform.isIOS)) {
|
||||
await windowManager.ensureInitialized();
|
||||
|
|
|
|||
|
|
@ -1,976 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart' as riv;
|
||||
import 'package:mangayomi/models/chapter.dart';
|
||||
import 'package:mangayomi/models/video.dart' as vid;
|
||||
import 'package:mangayomi/models/video.dart';
|
||||
import 'package:mangayomi/modules/anime/providers/anime_player_controller_provider.dart';
|
||||
import 'package:mangayomi/modules/anime/fvp/widgets/aniskip_countdown_btn.dart';
|
||||
import 'package:mangayomi/modules/anime/fvp/widgets/desktop.dart';
|
||||
import 'package:mangayomi/modules/anime/fvp/widgets/mobile.dart';
|
||||
import 'package:mangayomi/modules/anime/widgets/subtitle_setting_widget.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/widgets/btn_chapter_list_dialog.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/providers/push_router.dart';
|
||||
import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart';
|
||||
import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart';
|
||||
import 'package:mangayomi/modules/widgets/progress_center.dart';
|
||||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:mangayomi/services/aniskip.dart';
|
||||
import 'package:mangayomi/services/get_video_list.dart';
|
||||
import 'package:mangayomi/services/http/m_client.dart';
|
||||
import 'package:mangayomi/services/torrent_server.dart';
|
||||
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||
import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class AnimePlayerViewFvp extends riv.ConsumerStatefulWidget {
|
||||
final Chapter episode;
|
||||
const AnimePlayerViewFvp({super.key, required this.episode});
|
||||
|
||||
@override
|
||||
riv.ConsumerState<AnimePlayerViewFvp> createState() =>
|
||||
_AnimePlayerViewFvpState();
|
||||
}
|
||||
|
||||
class _AnimePlayerViewFvpState extends riv.ConsumerState<AnimePlayerViewFvp> {
|
||||
String? _infoHash;
|
||||
@override
|
||||
void dispose() {
|
||||
MTorrentServer().removeTorrent(_infoHash);
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final serversData =
|
||||
ref.watch(getVideoListProvider(episode: widget.episode));
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||
return serversData.when(
|
||||
data: (data) {
|
||||
_infoHash = data.$3;
|
||||
if (data.$1.isEmpty &&
|
||||
!(widget.episode.manga.value!.isLocalArchive ?? false)) {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: AppBar(
|
||||
title: const Text(''),
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
body: const Center(
|
||||
child: Text("Error"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return AnimeStreamPage(
|
||||
episode: widget.episode,
|
||||
videos: data.$1,
|
||||
isLocal: data.$2,
|
||||
);
|
||||
},
|
||||
error: (error, stackTrace) => Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: AppBar(
|
||||
title: const Text(''),
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
body: Center(
|
||||
child: Text(error.toString()),
|
||||
),
|
||||
),
|
||||
loading: () {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
title: const Text(''),
|
||||
leading: BackButton(
|
||||
color: Colors.white,
|
||||
onPressed: () {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
body: const ProgressCenter(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnimeStreamPage extends riv.ConsumerStatefulWidget {
|
||||
final List<vid.Video> videos;
|
||||
final Chapter episode;
|
||||
final bool isLocal;
|
||||
const AnimeStreamPage(
|
||||
{super.key,
|
||||
required this.isLocal,
|
||||
required this.videos,
|
||||
required this.episode});
|
||||
|
||||
@override
|
||||
riv.ConsumerState<AnimeStreamPage> createState() => _AnimeStreamPageState();
|
||||
}
|
||||
|
||||
class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
|
||||
with TickerProviderStateMixin {
|
||||
late Video _selectedVideo = widget.videos.first;
|
||||
late final List<Track> _subtitles = _selectedVideo.subtitles ?? [];
|
||||
late Track? _selectedSubtitle =
|
||||
_subtitles.isNotEmpty ? _subtitles.first : null;
|
||||
late VideoPlayerController _controller =
|
||||
_load(_selectedVideo, _selectedSubtitle);
|
||||
|
||||
VideoPlayerController _load(Video video, Track? subtitle) {
|
||||
return video.originalUrl.startsWith('http')
|
||||
? VideoPlayerController.networkUrl(Uri.parse(video.originalUrl),
|
||||
httpHeaders: video.headers ?? {},
|
||||
closedCaptionFile:
|
||||
subtitle != null ? _loadCaption(subtitle.file!) : null)
|
||||
: VideoPlayerController.file(File(video.originalUrl),
|
||||
httpHeaders: video.headers ?? {},
|
||||
closedCaptionFile:
|
||||
subtitle != null ? _loadCaption(subtitle.file!) : null);
|
||||
}
|
||||
|
||||
late final _streamController =
|
||||
ref.read(animeStreamControllerProvider(episode: widget.episode).notifier);
|
||||
|
||||
final ValueNotifier<double> _playbackSpeed = ValueNotifier(1.0);
|
||||
final ValueNotifier<bool> _enterFullScreen = ValueNotifier(false);
|
||||
late final ValueNotifier<Duration> _currentPosition =
|
||||
ValueNotifier(_streamController.geTCurrentPosition());
|
||||
final ValueNotifier<Duration?> _currentTotalDuration = ValueNotifier(null);
|
||||
final ValueNotifier<bool> _isCompleted = ValueNotifier(false);
|
||||
final ValueNotifier<Duration?> _tempPosition = ValueNotifier(null);
|
||||
|
||||
Results? _openingResult;
|
||||
Results? _endingResult;
|
||||
bool _hasOpeningSkip = false;
|
||||
bool _hasEndingSkip = false;
|
||||
final ValueNotifier<bool> _showAniSkipOpeningButton = ValueNotifier(false);
|
||||
final ValueNotifier<bool> _showAniSkipEndingButton = ValueNotifier(false);
|
||||
|
||||
Future<ClosedCaptionFile>? _loadCaption(String url) async {
|
||||
String fileContents = "";
|
||||
fileContents =
|
||||
utf8.decode((await MClient.init().get(Uri.parse(url))).bodyBytes);
|
||||
if (url.endsWith(".srt")) {
|
||||
return SubRipCaptionFile(fileContents);
|
||||
}
|
||||
return WebVTTCaptionFile(fileContents);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_setCurrentPosition(true);
|
||||
_listener();
|
||||
_controller.initialize().then((_) => setState(() {}));
|
||||
|
||||
_initAniSkip();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
bool _isInitiated = false;
|
||||
void _listener({Duration? duration}) {
|
||||
_controller.addListener(() {
|
||||
setState(() {});
|
||||
if (_controller.value.isPlaying && !_isInitiated) {
|
||||
_seekToCurrentPosition(duration: duration);
|
||||
_isInitiated = true;
|
||||
}
|
||||
_isCompleted.value = _controller.value.duration.inSeconds -
|
||||
_currentPosition.value.inSeconds <=
|
||||
10;
|
||||
_currentPosition.value = _controller.value.position;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _seekToCurrentPosition({Duration? duration}) async {
|
||||
await _controller.play();
|
||||
await _controller
|
||||
.seekTo(duration ?? _streamController.geTCurrentPosition());
|
||||
}
|
||||
|
||||
void _initAniSkip() async {
|
||||
_streamController.getAniSkipResults((result) {
|
||||
final openingRes =
|
||||
result.where((element) => element.skipType == "op").toList();
|
||||
_hasOpeningSkip = openingRes.isNotEmpty;
|
||||
if (_hasOpeningSkip) {
|
||||
_openingResult = openingRes.first;
|
||||
}
|
||||
final endingRes =
|
||||
result.where((element) => element.skipType == "ed").toList();
|
||||
_hasEndingSkip = endingRes.isNotEmpty;
|
||||
if (_hasEndingSkip) {
|
||||
_endingResult = endingRes.first;
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool isDesktop = Platform.isMacOS || Platform.isLinux || Platform.isWindows;
|
||||
@override
|
||||
void dispose() {
|
||||
_setCurrentPosition(true);
|
||||
if (isDesktop) {
|
||||
setFullScreen(value: false);
|
||||
} else {
|
||||
_setLandscapeMode(false);
|
||||
}
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _setCurrentPosition(bool save) {
|
||||
_streamController.setCurrentPosition(
|
||||
_currentPosition.value, _currentTotalDuration.value,
|
||||
save: save);
|
||||
_streamController.setAnimeHistoryUpdate();
|
||||
}
|
||||
|
||||
void _setLandscapeMode(bool state) {
|
||||
if (state) {
|
||||
SystemChrome.setPreferredOrientations(
|
||||
[DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
|
||||
} else {
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Widget textWidget(String text, bool selected) => Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.of(context).padding.top),
|
||||
child: Text(text,
|
||||
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
|
||||
fontSize: 16,
|
||||
fontStyle: selected ? FontStyle.italic : null,
|
||||
color: selected ? context.primaryColor : null),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
)),
|
||||
],
|
||||
);
|
||||
|
||||
Future<void> _setPlaybackSpeed(double speed) async {
|
||||
await _controller.setPlaybackSpeed(speed);
|
||||
_playbackSpeed.value = speed;
|
||||
}
|
||||
|
||||
void _togglePlaybackSpeed() {
|
||||
List<double> allowedSpeeds = [0.25, 0.5, 0.75, 1.0, 1.25, 1.50, 1.75, 2.0];
|
||||
if (allowedSpeeds.indexOf(_playbackSpeed.value) <
|
||||
allowedSpeeds.length - 1) {
|
||||
_setPlaybackSpeed(
|
||||
allowedSpeeds[allowedSpeeds.indexOf(_playbackSpeed.value) + 1]);
|
||||
} else {
|
||||
_setPlaybackSpeed(allowedSpeeds[0]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _changeFitLabel(WidgetRef ref) async {
|
||||
// List<BoxFit> fitList = [
|
||||
// BoxFit.contain,
|
||||
// BoxFit.cover,
|
||||
// BoxFit.fill,
|
||||
// BoxFit.fitHeight,
|
||||
// BoxFit.fitWidth,
|
||||
// BoxFit.scaleDown,
|
||||
// BoxFit.none
|
||||
// ];
|
||||
// _showFitLabel.value = true;
|
||||
// BoxFit? fit;
|
||||
// if (fitList.indexOf(_fit.value) < fitList.length - 1) {
|
||||
// fit = fitList[fitList.indexOf(_fit.value) + 1];
|
||||
// } else {
|
||||
// fit = fitList[0];
|
||||
// }
|
||||
// _fit.value = fit;
|
||||
// _controller.currentState?.update(fit: fit);
|
||||
// BotToast.showText(
|
||||
// onlyOne: true,
|
||||
// align: const Alignment(0, 0.90),
|
||||
// duration: const Duration(seconds: 1),
|
||||
// text: fit.name.toUpperCase());
|
||||
}
|
||||
|
||||
Widget _seekToWidget() {
|
||||
final defaultSkipIntroLength =
|
||||
ref.watch(defaultSkipIntroLengthStateProvider);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||
child: SizedBox(
|
||||
height: 35,
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
_tempPosition.value = Duration(
|
||||
seconds: defaultSkipIntroLength +
|
||||
_currentPosition.value.inSeconds);
|
||||
await _controller.seekTo(Duration(
|
||||
seconds: _currentPosition.value.inSeconds +
|
||||
defaultSkipIntroLength));
|
||||
_tempPosition.value = null;
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text("+$defaultSkipIntroLength",
|
||||
style: const TextStyle(fontWeight: FontWeight.w100)),
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _mobileBottomButtonBar(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 30),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_seekToWidget(),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
padding: const EdgeInsets.all(5),
|
||||
onPressed: () => _videoSettingDraggableMenu(context),
|
||||
icon: const Icon(
|
||||
Icons.video_settings,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
child: ValueListenableBuilder<double>(
|
||||
valueListenable: _playbackSpeed,
|
||||
builder: (context, value, child) {
|
||||
return Text(
|
||||
"${value}x",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
);
|
||||
},
|
||||
),
|
||||
onPressed: () {
|
||||
_togglePlaybackSpeed();
|
||||
}),
|
||||
// IconButton(
|
||||
// icon: const Icon(Icons.fit_screen_outlined,
|
||||
// color: Colors.white),
|
||||
// onPressed: () async {
|
||||
// _changeFitLabel(ref);
|
||||
// },
|
||||
// ),
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: _enterFullScreen,
|
||||
builder: (context, snapshot, _) {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
_setLandscapeMode(!snapshot);
|
||||
_enterFullScreen.value = !snapshot;
|
||||
},
|
||||
icon: Icon(snapshot
|
||||
? Icons.fullscreen_exit
|
||||
: Icons.fullscreen),
|
||||
iconSize: 25,
|
||||
color: Colors.white,
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _desktopBottomButtonBar(BuildContext context) {
|
||||
bool hasPrevEpisode = _streamController.getEpisodeIndex().$1 + 1 !=
|
||||
_streamController
|
||||
.getEpisodesLength(_streamController.getEpisodeIndex().$2);
|
||||
bool hasNextEpisode = _streamController.getEpisodeIndex().$1 != 0;
|
||||
final skipDuration = ref.watch(defaultDoubleTapToSkipLengthStateProvider);
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (hasPrevEpisode)
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
if (isDesktop) {
|
||||
final isFullScreen = await windowManager.isFullScreen();
|
||||
if (isFullScreen) {
|
||||
await setFullScreen(value: false);
|
||||
}
|
||||
}
|
||||
if (context.mounted) {
|
||||
pushReplacementMangaReaderView(
|
||||
context: context,
|
||||
chapter: _streamController.getPrevEpisode());
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.skip_previous,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
CustomeMaterialDesktopPlayOrPauseButton(
|
||||
controller: _controller,
|
||||
),
|
||||
if (hasNextEpisode)
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
if (isDesktop) {
|
||||
final isFullScreen = await windowManager.isFullScreen();
|
||||
if (isFullScreen) {
|
||||
await setFullScreen(value: false);
|
||||
}
|
||||
}
|
||||
if (context.mounted) {
|
||||
pushReplacementMangaReaderView(
|
||||
context: context,
|
||||
chapter: _streamController.getNextEpisode(),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.skip_next, color: Colors.white),
|
||||
),
|
||||
SizedBox(
|
||||
height: 50,
|
||||
width: 50,
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
_tempPosition.value = Duration(
|
||||
seconds:
|
||||
skipDuration - _currentPosition.value.inSeconds);
|
||||
await _controller.seekTo(Duration(
|
||||
seconds:
|
||||
_currentPosition.value.inSeconds - skipDuration));
|
||||
_tempPosition.value = null;
|
||||
},
|
||||
icon: Stack(
|
||||
children: [
|
||||
const Positioned.fill(
|
||||
child: Icon(
|
||||
Icons.rotate_left_outlined,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
skipDuration.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 9, color: Colors.white),
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 50,
|
||||
width: 50,
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
_tempPosition.value = Duration(
|
||||
seconds:
|
||||
skipDuration + _currentPosition.value.inSeconds);
|
||||
await _controller.seekTo(Duration(
|
||||
seconds:
|
||||
_currentPosition.value.inSeconds + skipDuration));
|
||||
_tempPosition.value = null;
|
||||
},
|
||||
icon: Stack(
|
||||
children: [
|
||||
const Positioned.fill(
|
||||
child: Icon(
|
||||
Icons.rotate_right_outlined,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
skipDuration.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 9, color: Colors.white),
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
CustomMaterialDesktopVolumeButton(
|
||||
controller: _controller,
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _tempPosition,
|
||||
builder: (context, value, child) =>
|
||||
CustomMaterialDesktopPositionIndicator(
|
||||
delta: value, controller: _controller),
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => _videoSettingDraggableMenu(context),
|
||||
icon: const Icon(
|
||||
Icons.video_settings,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
child: ValueListenableBuilder<double>(
|
||||
valueListenable: _playbackSpeed,
|
||||
builder: (context, value, child) {
|
||||
return Text(
|
||||
"${value}x",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
);
|
||||
},
|
||||
),
|
||||
onPressed: () {
|
||||
_togglePlaybackSpeed();
|
||||
}),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.fit_screen_outlined,
|
||||
color: Colors.white),
|
||||
onPressed: () async {
|
||||
_changeFitLabel(ref);
|
||||
},
|
||||
),
|
||||
CustomMaterialDesktopFullscreenButton(
|
||||
controller: _controller,
|
||||
isFullscreen: (v) {},
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _videoQualityWidget(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 12),
|
||||
child: Column(
|
||||
children: widget.videos.map((quality) {
|
||||
final selected = quality == _selectedVideo || widget.isLocal;
|
||||
return GestureDetector(
|
||||
child: textWidget(
|
||||
widget.isLocal ? _selectedVideo.quality : quality.quality,
|
||||
selected),
|
||||
onTap: () async {
|
||||
if (widget.isLocal) {
|
||||
} else {
|
||||
_controller = VideoPlayerController.networkUrl(
|
||||
Uri.parse(quality.originalUrl),
|
||||
httpHeaders: quality.headers ?? {},
|
||||
closedCaptionFile: _selectedSubtitle != null
|
||||
? _loadCaption(_selectedSubtitle!.file!)
|
||||
: null);
|
||||
}
|
||||
setState(() {
|
||||
_isInitiated = false;
|
||||
});
|
||||
_listener(duration: _currentPosition.value);
|
||||
_selectedVideo = quality;
|
||||
await _controller.initialize();
|
||||
if (context.mounted) {
|
||||
setState(() {});
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _videoSettingDraggableMenu(BuildContext context) async {
|
||||
final l10n = l10nLocalizations(context)!;
|
||||
bool hasSubtitleTrack = false;
|
||||
_controller.pause();
|
||||
await customDraggableTabBar(
|
||||
tabs: [
|
||||
Tab(text: l10n.video_quality),
|
||||
Tab(text: l10n.video_subtitle),
|
||||
Tab(text: l10n.video_audio),
|
||||
],
|
||||
children: [
|
||||
_videoQualityWidget(context),
|
||||
_videoSubtitle(context, (value) => hasSubtitleTrack = value),
|
||||
_videoAudios(context)
|
||||
],
|
||||
context: context,
|
||||
vsync: this,
|
||||
fullWidth: true,
|
||||
moreWidget: IconButton(
|
||||
onPressed: () async {
|
||||
await customDraggableTabBar(tabs: [
|
||||
Tab(text: l10n.font),
|
||||
Tab(text: l10n.color),
|
||||
], children: [
|
||||
FontSettingWidget(hasSubtitleTrack: hasSubtitleTrack),
|
||||
ColorSettingWidget(hasSubtitleTrack: hasSubtitleTrack)
|
||||
], context: context, vsync: this, fullWidth: true);
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.settings_outlined)),
|
||||
);
|
||||
setState(() {});
|
||||
_controller.play();
|
||||
}
|
||||
|
||||
Widget _videoSubtitle(BuildContext context, Function(bool) hasSubtitleTrack) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 12),
|
||||
child: Column(
|
||||
children: (_selectedVideo.subtitles ?? []).toList().map((sub) {
|
||||
final title = sub.label!;
|
||||
|
||||
final selected = sub == _selectedSubtitle;
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
try {
|
||||
await _controller.setClosedCaptionFile(_loadCaption(sub.file!));
|
||||
_listener();
|
||||
if (context.mounted) {
|
||||
setState(() {
|
||||
_selectedSubtitle = sub;
|
||||
});
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} catch (_) {}
|
||||
},
|
||||
child: textWidget(title, selected),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _videoAudios(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 12),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: textWidget("#1", true)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _topButtonBar(BuildContext context) {
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: _enterFullScreen,
|
||||
builder: (context, fullScreen, _) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: !isDesktop && !fullScreen
|
||||
? MediaQuery.of(context).padding.top
|
||||
: 0),
|
||||
child: Row(
|
||||
children: [
|
||||
BackButton(
|
||||
color: Colors.white,
|
||||
onPressed: () async {
|
||||
if (isDesktop) {
|
||||
if (fullScreen) {
|
||||
setFullScreen(value: false);
|
||||
} else {
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
Flexible(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: SizedBox(
|
||||
width: context.width(0.8),
|
||||
child: Text(
|
||||
widget.episode.manga.value!.name!,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, color: Colors.white),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
subtitle: SizedBox(
|
||||
width: context.width(0.8),
|
||||
child: Text(
|
||||
widget.episode.name!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.white.withOpacity(0.7)),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(children: [
|
||||
btnToShowChapterListDialog(
|
||||
context,
|
||||
context.l10n.episodes,
|
||||
widget.episode,
|
||||
onChanged: (v) {
|
||||
if (v) {
|
||||
_controller.play();
|
||||
} else {
|
||||
_controller.pause();
|
||||
}
|
||||
},
|
||||
),
|
||||
// IconButton(
|
||||
// onPressed: () {
|
||||
// showDialog(
|
||||
// context: context,
|
||||
// builder: (context) {
|
||||
// return AlertDialog(
|
||||
// scrollable: true,
|
||||
// title: const Text("Player Settings"),
|
||||
// content: SizedBox(
|
||||
// width: context.width(0.8),
|
||||
// child: Column(
|
||||
// crossAxisAlignment:
|
||||
// CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// SwitchListTile(
|
||||
// value: false,
|
||||
// title: Text(
|
||||
// "Enable Volume and Brightness Gestures",
|
||||
// style: TextStyle(
|
||||
// color: Theme.of(context)
|
||||
// .textTheme
|
||||
// .bodyLarge!
|
||||
// .color!
|
||||
// .withOpacity(0.9),
|
||||
// fontSize: 14),
|
||||
// ),
|
||||
// onChanged: (value) {}),
|
||||
// SwitchListTile(
|
||||
// value: false,
|
||||
// title: Text(
|
||||
// "Enable Horizonal Seek Gestures",
|
||||
// style: TextStyle(
|
||||
// color: Theme.of(context)
|
||||
// .textTheme
|
||||
// .bodyLarge!
|
||||
// .color!
|
||||
// .withOpacity(0.9),
|
||||
// fontSize: 14),
|
||||
// ),
|
||||
// onChanged: (value) {}),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// });
|
||||
// },
|
||||
// icon: Icon(Icons.adaptive.more))
|
||||
])
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _videoPlayer(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: AspectRatio(
|
||||
aspectRatio: _controller.value.aspectRatio,
|
||||
child: VideoPlayer(_controller)),
|
||||
),
|
||||
isDesktop
|
||||
? DesktopControllerWidget(
|
||||
videoController: _controller,
|
||||
topButtonBarWidget: _topButtonBar(context),
|
||||
bottomButtonBarWidget: _desktopBottomButtonBar(context),
|
||||
streamController: _streamController,
|
||||
seekToWidget: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: Row(
|
||||
children: [
|
||||
_seekToWidget(),
|
||||
],
|
||||
),
|
||||
),
|
||||
tempDuration: (value) {
|
||||
_tempPosition.value = value;
|
||||
},
|
||||
isFullScreen: false,
|
||||
)
|
||||
: MobileControllerWidget(
|
||||
videoController: _controller,
|
||||
topButtonBarWidget: _topButtonBar(context),
|
||||
isFullScreen: false,
|
||||
bottomButtonBarWidget: _mobileBottomButtonBar(context),
|
||||
streamController: _streamController,
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 80,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _currentPosition,
|
||||
builder: (context, value, child) {
|
||||
if (_hasOpeningSkip || _hasEndingSkip) {
|
||||
if (_hasOpeningSkip) {
|
||||
if (_openingResult!.interval!.startTime!.ceil() <=
|
||||
value.inSeconds &&
|
||||
_openingResult!.interval!.endTime!.toInt() >
|
||||
value.inSeconds) {
|
||||
_showAniSkipOpeningButton.value = true;
|
||||
_showAniSkipEndingButton.value = false;
|
||||
} else {
|
||||
_showAniSkipOpeningButton.value = false;
|
||||
}
|
||||
}
|
||||
if (_hasEndingSkip) {
|
||||
if (_endingResult!.interval!.startTime!.ceil() <=
|
||||
value.inSeconds &&
|
||||
_endingResult!.interval!.endTime!.toInt() >
|
||||
value.inSeconds) {
|
||||
_showAniSkipEndingButton.value = true;
|
||||
_showAniSkipOpeningButton.value = false;
|
||||
}
|
||||
} else {
|
||||
_showAniSkipEndingButton.value = false;
|
||||
}
|
||||
}
|
||||
return Consumer(builder: (context, ref, _) {
|
||||
late final enableAniSkip =
|
||||
ref.watch(enableAniSkipStateProvider);
|
||||
late final enableAutoSkip =
|
||||
ref.watch(enableAutoSkipStateProvider);
|
||||
late final aniSkipTimeoutLength =
|
||||
ref.watch(aniSkipTimeoutLengthStateProvider);
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _showAniSkipOpeningButton,
|
||||
builder: (context, showAniSkipOpENINGButton, child) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _showAniSkipEndingButton,
|
||||
builder: (context, showAniSkipENDINGButton, child) {
|
||||
return showAniSkipOpENINGButton
|
||||
? Container(
|
||||
key: const Key('skip_opening'),
|
||||
child: AniSkipCountDownButton(
|
||||
active: enableAniSkip,
|
||||
autoSkip: enableAutoSkip,
|
||||
timeoutLength: aniSkipTimeoutLength,
|
||||
skipTypeText: context.l10n.skip_opening,
|
||||
controller: _controller,
|
||||
aniSkipResult: _openingResult,
|
||||
))
|
||||
: showAniSkipENDINGButton
|
||||
? Container(
|
||||
key: const Key('skip_ending'),
|
||||
child: AniSkipCountDownButton(
|
||||
active: enableAniSkip,
|
||||
autoSkip: enableAutoSkip,
|
||||
timeoutLength: aniSkipTimeoutLength,
|
||||
skipTypeText: context.l10n.skip_ending,
|
||||
controller: _controller,
|
||||
aniSkipResult: _endingResult,
|
||||
))
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: _videoPlayer(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget seekIndicatorTextWidget(Duration duration, Duration currentPosition) {
|
||||
final swipeDuration = duration.inSeconds;
|
||||
final value = currentPosition.inSeconds + swipeDuration;
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
Duration(seconds: value).label(),
|
||||
style: const TextStyle(
|
||||
fontSize: 65.0, fontWeight: FontWeight.bold, color: Colors.white),
|
||||
),
|
||||
Text(
|
||||
"[${swipeDuration > 0 ? "+${Duration(seconds: swipeDuration).label()}" : "-${Duration(seconds: swipeDuration).label()}"}]",
|
||||
style: const TextStyle(
|
||||
fontSize: 40.0, color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mangayomi/services/aniskip.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
class AniSkipCountDownButton extends ConsumerStatefulWidget {
|
||||
final bool active;
|
||||
final bool autoSkip;
|
||||
final int timeoutLength;
|
||||
final String skipTypeText;
|
||||
final Results? aniSkipResult;
|
||||
final VideoPlayerController controller;
|
||||
const AniSkipCountDownButton(
|
||||
{super.key,
|
||||
required this.skipTypeText,
|
||||
required this.aniSkipResult,
|
||||
required this.controller,
|
||||
required this.active,
|
||||
required this.autoSkip,
|
||||
required this.timeoutLength});
|
||||
|
||||
@override
|
||||
ConsumerState<AniSkipCountDownButton> createState() =>
|
||||
_AniSkipCountDownButtonState();
|
||||
}
|
||||
|
||||
class _AniSkipCountDownButtonState extends ConsumerState<AniSkipCountDownButton>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
@override
|
||||
void initState() {
|
||||
_controller = AnimationController(
|
||||
vsync: this, duration: Duration(seconds: widget.timeoutLength))
|
||||
..forward();
|
||||
super.initState();
|
||||
if (widget.active) {
|
||||
if (widget.autoSkip) {
|
||||
_seekTo();
|
||||
} else {
|
||||
_controller.addListener(() {
|
||||
if (_controller.isCompleted) {
|
||||
setState(() {
|
||||
_isCompleted = true;
|
||||
});
|
||||
_controller.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _seekTo() {
|
||||
setState(() {
|
||||
_isCompleted = true;
|
||||
});
|
||||
_controller.reset();
|
||||
widget.controller.seekTo(
|
||||
Duration(seconds: widget.aniSkipResult!.interval!.endTime!.ceil()));
|
||||
}
|
||||
|
||||
bool _isCompleted = false;
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.active && !widget.autoSkip
|
||||
? _isCompleted
|
||||
? const SizedBox.shrink()
|
||||
: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: MaterialButton(
|
||||
padding: const EdgeInsets.all(0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5)),
|
||||
onPressed: () {
|
||||
_seekTo();
|
||||
},
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
width: 200,
|
||||
child: Stack(
|
||||
children: [
|
||||
RotatedBox(
|
||||
quarterTurns: 0,
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: SizedBox.fromSize(
|
||||
size: const Size(200, 40),
|
||||
child: LinearProgressIndicator(
|
||||
color: Colors.red,
|
||||
value: 1 - _controller.value,
|
||||
backgroundColor: Colors.transparent)),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Text(
|
||||
widget.skipTypeText.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text((widget.timeoutLength -
|
||||
(_controller.duration! *
|
||||
_controller.value)
|
||||
.inSeconds)
|
||||
.toString()),
|
||||
],
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
: const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
class CustomSeekBar extends StatefulWidget {
|
||||
final VideoPlayerController controller;
|
||||
final Duration? delta;
|
||||
final Function(Duration)? onSeekStart;
|
||||
final Function(Duration)? onSeekEnd;
|
||||
|
||||
const CustomSeekBar(
|
||||
{super.key,
|
||||
this.onSeekStart,
|
||||
this.onSeekEnd,
|
||||
required this.controller,
|
||||
this.delta});
|
||||
|
||||
@override
|
||||
CustomSeekBarState createState() => CustomSeekBarState();
|
||||
}
|
||||
|
||||
class CustomSeekBarState extends State<CustomSeekBar> {
|
||||
Duration? tempPosition;
|
||||
late VideoPlayerController controller = widget.controller;
|
||||
Duration position = Duration.zero;
|
||||
late Duration duration = controller.value.duration;
|
||||
Duration buffer = Duration.zero;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller.addListener(() {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
controller.removeListener(() {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
final isDesktop = Platform.isMacOS || Platform.isWindows || Platform.isLinux;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
duration = controller.value.duration;
|
||||
position = controller.value.position;
|
||||
|
||||
for (final DurationRange range in controller.value.buffered) {
|
||||
final end = range.end;
|
||||
if (end > buffer) {
|
||||
buffer = end;
|
||||
}
|
||||
}
|
||||
return SizedBox(
|
||||
height: 20,
|
||||
child: Row(
|
||||
children: [
|
||||
if (!isDesktop)
|
||||
SizedBox(
|
||||
width: 70,
|
||||
child: Center(
|
||||
child: Text(
|
||||
(widget.delta ?? tempPosition ?? position)
|
||||
.label(reference: duration),
|
||||
style: const TextStyle(
|
||||
height: 1.0,
|
||||
fontSize: 12.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
))),
|
||||
Expanded(
|
||||
child: SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
trackHeight: isDesktop ? null : 3,
|
||||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 5.0),
|
||||
),
|
||||
child: Slider(
|
||||
max: max(duration.inMilliseconds.toDouble(), 0),
|
||||
value: max(
|
||||
(widget.delta ?? tempPosition ?? position)
|
||||
.inMilliseconds
|
||||
.toDouble(),
|
||||
0),
|
||||
secondaryTrackValue: max(buffer.inMilliseconds.toDouble(), 0),
|
||||
onChanged: (value) {
|
||||
widget.onSeekStart?.call(Duration(
|
||||
milliseconds: value.toInt() - position.inMilliseconds));
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
tempPosition = Duration(milliseconds: value.toInt());
|
||||
});
|
||||
}
|
||||
},
|
||||
onChangeEnd: (value) async {
|
||||
widget.onSeekEnd?.call(Duration(
|
||||
milliseconds: value.toInt() - position.inMilliseconds));
|
||||
widget.controller
|
||||
.seekTo(Duration(milliseconds: value.toInt()));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isDesktop)
|
||||
SizedBox(
|
||||
width: 70,
|
||||
child: Center(
|
||||
child: Text(
|
||||
duration.label(reference: duration),
|
||||
style: const TextStyle(
|
||||
height: 1.0,
|
||||
fontSize: 12.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,771 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mangayomi/modules/anime/anime_player_view.dart';
|
||||
import 'package:mangayomi/modules/anime/providers/anime_player_controller_provider.dart';
|
||||
import 'package:mangayomi/modules/anime/fvp/widgets/custom_seekbar.dart';
|
||||
import 'package:mangayomi/modules/anime/fvp/widgets/subtitle_view.dart';
|
||||
import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class DesktopControllerWidget extends StatefulWidget {
|
||||
final Function(Duration?) tempDuration;
|
||||
final AnimeStreamController streamController;
|
||||
final VideoPlayerController videoController;
|
||||
final Widget topButtonBarWidget;
|
||||
final Widget bottomButtonBarWidget;
|
||||
final Widget seekToWidget;
|
||||
final bool isFullScreen;
|
||||
const DesktopControllerWidget(
|
||||
{super.key,
|
||||
required this.videoController,
|
||||
required this.topButtonBarWidget,
|
||||
required this.bottomButtonBarWidget,
|
||||
required this.streamController,
|
||||
required this.seekToWidget,
|
||||
required this.tempDuration,
|
||||
required this.isFullScreen});
|
||||
|
||||
@override
|
||||
State<DesktopControllerWidget> createState() =>
|
||||
_DesktopControllerWidgetState();
|
||||
}
|
||||
|
||||
class _DesktopControllerWidgetState extends State<DesktopControllerWidget> {
|
||||
bool mount = true;
|
||||
bool visible = true;
|
||||
Duration controlsTransitionDuration = const Duration(milliseconds: 300);
|
||||
Color backdropColor = const Color(0x66000000);
|
||||
Timer? _timer;
|
||||
|
||||
int swipeDuration = 0; // Duration to seek in video
|
||||
bool showSwipeDuration = false; // Whether to show the seek duration overlay
|
||||
|
||||
late bool buffering = widget.videoController.value.isBuffering;
|
||||
final controlsHoverDuration = const Duration(seconds: 3);
|
||||
double buttonBarHeight = 100;
|
||||
final bottomButtonBarMargin = const EdgeInsets.only(left: 16.0, right: 8.0);
|
||||
|
||||
DateTime last = DateTime.now();
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (!mounted) return;
|
||||
widget.videoController.addListener(
|
||||
() {
|
||||
setState(() {
|
||||
buffering = widget.videoController.value.isBuffering;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
_timer = Timer(
|
||||
controlsHoverDuration,
|
||||
() {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
visible = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void onHover() {
|
||||
setState(() {
|
||||
mount = true;
|
||||
visible = true;
|
||||
});
|
||||
|
||||
_timer?.cancel();
|
||||
_timer = Timer(controlsHoverDuration, () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
visible = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onEnter() {
|
||||
setState(() {
|
||||
mount = true;
|
||||
visible = true;
|
||||
});
|
||||
|
||||
_timer?.cancel();
|
||||
_timer = Timer(controlsHoverDuration, () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
visible = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onExit() {
|
||||
setState(() {
|
||||
visible = false;
|
||||
});
|
||||
|
||||
_timer?.cancel();
|
||||
}
|
||||
|
||||
final bool modifyVolumeOnScroll = true;
|
||||
final bool toggleFullscreenOnDoublePress = true;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CallbackShortcuts(
|
||||
bindings: {
|
||||
const SingleActivator(LogicalKeyboardKey.mediaPlay): () =>
|
||||
widget.videoController.play(),
|
||||
const SingleActivator(LogicalKeyboardKey.mediaPause): () =>
|
||||
widget.videoController.pause(),
|
||||
const SingleActivator(LogicalKeyboardKey.mediaPlayPause): () =>
|
||||
widget.videoController.value.isPlaying
|
||||
? widget.videoController.pause()
|
||||
: widget.videoController.play(),
|
||||
const SingleActivator(LogicalKeyboardKey.space): () =>
|
||||
widget.videoController.value.isPlaying
|
||||
? widget.videoController.pause()
|
||||
: widget.videoController.play(),
|
||||
const SingleActivator(LogicalKeyboardKey.keyJ): () {
|
||||
final rate = widget.videoController.value.position -
|
||||
const Duration(seconds: 10);
|
||||
widget.videoController.seekTo(rate);
|
||||
},
|
||||
const SingleActivator(LogicalKeyboardKey.keyI): () {
|
||||
final rate = widget.videoController.value.position +
|
||||
const Duration(seconds: 10);
|
||||
widget.videoController.seekTo(rate);
|
||||
},
|
||||
const SingleActivator(LogicalKeyboardKey.arrowLeft): () {
|
||||
final rate = widget.videoController.value.position -
|
||||
const Duration(seconds: 2);
|
||||
widget.videoController.seekTo(rate);
|
||||
},
|
||||
const SingleActivator(LogicalKeyboardKey.arrowRight): () {
|
||||
final rate = widget.videoController.value.position +
|
||||
const Duration(seconds: 2);
|
||||
widget.videoController.seekTo(rate);
|
||||
},
|
||||
const SingleActivator(LogicalKeyboardKey.arrowUp): () {
|
||||
final volume = widget.videoController.value.volume + 5.0;
|
||||
widget.videoController.setVolume(volume.clamp(0.0, 100.0));
|
||||
},
|
||||
const SingleActivator(LogicalKeyboardKey.arrowDown): () {
|
||||
final volume = widget.videoController.value.volume - 5.0;
|
||||
widget.videoController.setVolume(volume.clamp(0.0, 100.0));
|
||||
},
|
||||
const SingleActivator(LogicalKeyboardKey.keyF): () => setFullScreen(),
|
||||
const SingleActivator(LogicalKeyboardKey.escape): () =>
|
||||
setFullScreen(value: false),
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Consumer(
|
||||
builder: (context, ref, _) => Positioned(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 30),
|
||||
child: CustomSubtitleView(
|
||||
controller: widget.videoController,
|
||||
),
|
||||
)),
|
||||
),
|
||||
Focus(
|
||||
autofocus: true,
|
||||
child: Listener(
|
||||
onPointerSignal: modifyVolumeOnScroll
|
||||
? (e) {
|
||||
if (e is PointerScrollEvent) {
|
||||
if (e.delta.dy > 0) {
|
||||
final volume =
|
||||
widget.videoController.value.volume - 5.0;
|
||||
widget.videoController
|
||||
.setVolume(volume.clamp(0.0, 100.0));
|
||||
}
|
||||
if (e.delta.dy < 0) {
|
||||
final volume =
|
||||
widget.videoController.value.volume + 5.0;
|
||||
widget.videoController
|
||||
.setVolume(volume.clamp(0.0, 100.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: GestureDetector(
|
||||
onTapUp: !toggleFullscreenOnDoublePress
|
||||
? null
|
||||
: (e) {
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(last);
|
||||
last = now;
|
||||
if (difference < const Duration(milliseconds: 400)) {
|
||||
setFullScreen();
|
||||
}
|
||||
},
|
||||
onPanUpdate: modifyVolumeOnScroll
|
||||
? (e) {
|
||||
if (e.delta.dy > 0) {
|
||||
final volume =
|
||||
widget.videoController.value.volume - 5.0;
|
||||
widget.videoController
|
||||
.setVolume(volume.clamp(0.0, 100.0));
|
||||
}
|
||||
if (e.delta.dy < 0) {
|
||||
final volume =
|
||||
widget.videoController.value.volume + 5.0;
|
||||
widget.videoController
|
||||
.setVolume(volume.clamp(0.0, 100.0));
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: MouseRegion(
|
||||
onHover: (_) => onHover(),
|
||||
onEnter: (_) => onEnter(),
|
||||
onExit: (_) => onExit(),
|
||||
child: Stack(
|
||||
children: [
|
||||
AnimatedOpacity(
|
||||
curve: Curves.easeInOut,
|
||||
opacity: visible ? 1.0 : 0.0,
|
||||
duration: controlsTransitionDuration,
|
||||
onEnd: () {
|
||||
if (!visible) {
|
||||
setState(() {
|
||||
mount = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
// Top gradient.
|
||||
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
stops: [
|
||||
0.0,
|
||||
0.2,
|
||||
],
|
||||
colors: [
|
||||
Color(0x61000000),
|
||||
Color(0x00000000),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Bottom gradient.
|
||||
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
stops: [
|
||||
0.5,
|
||||
1.0,
|
||||
],
|
||||
colors: [
|
||||
Color(0x00000000),
|
||||
Color(0x61000000),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (mount)
|
||||
Padding(
|
||||
padding: (widget.isFullScreen
|
||||
? MediaQuery.of(context).padding
|
||||
: EdgeInsets.zero),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
widget.topButtonBarWidget,
|
||||
// Only display [primaryButtonBar] if [buffering] is false.
|
||||
Expanded(
|
||||
child: AnimatedOpacity(
|
||||
curve: Curves.easeInOut,
|
||||
opacity: buffering
|
||||
? 0.0
|
||||
: !showSwipeDuration
|
||||
? 0.0
|
||||
: 1.0,
|
||||
duration: controlsTransitionDuration,
|
||||
child: Center(
|
||||
child: seekIndicatorTextWidget(
|
||||
Duration(
|
||||
seconds: swipeDuration),
|
||||
widget.videoController.value
|
||||
.position))),
|
||||
),
|
||||
widget.seekToWidget,
|
||||
Transform.translate(
|
||||
offset: Offset.zero,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 5),
|
||||
child: CustomSeekBar(
|
||||
onSeekStart: (value) {
|
||||
setState(() {
|
||||
swipeDuration = value.inSeconds;
|
||||
showSwipeDuration = true;
|
||||
widget.tempDuration(widget
|
||||
.videoController
|
||||
.value
|
||||
.position +
|
||||
value);
|
||||
});
|
||||
_timer?.cancel();
|
||||
},
|
||||
onSeekEnd: (value) {
|
||||
_timer = Timer(
|
||||
controlsHoverDuration,
|
||||
() {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
visible = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
setState(() {
|
||||
showSwipeDuration = false;
|
||||
});
|
||||
widget.tempDuration(null);
|
||||
},
|
||||
controller: widget.videoController,
|
||||
),
|
||||
),
|
||||
),
|
||||
widget.bottomButtonBarWidget
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Buffering Indicator.
|
||||
IgnorePointer(
|
||||
child: Padding(
|
||||
padding: (widget.isFullScreen
|
||||
? MediaQuery.of(context).padding
|
||||
: EdgeInsets.zero),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
height: buttonBarHeight,
|
||||
margin: const EdgeInsets.all(0),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Center(
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(
|
||||
begin: 0.0,
|
||||
end: buffering ? 1.0 : 0.0,
|
||||
),
|
||||
duration: controlsTransitionDuration,
|
||||
builder: (context, value, child) {
|
||||
// Only mount the buffering indicator if the opacity is greater than 0.0.
|
||||
// This has been done to prevent redundant resource usage in [CircularProgressIndicator].
|
||||
if (value > 0.0) {
|
||||
return Opacity(
|
||||
opacity: value,
|
||||
child: child!,
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
child: const CircularProgressIndicator(
|
||||
color: Color(0xFFFFFFFF),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: buttonBarHeight,
|
||||
margin: bottomButtonBarMargin,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// BUTTON: PLAY/PAUSE
|
||||
|
||||
/// A material design play/pause button.
|
||||
class CustomeMaterialDesktopPlayOrPauseButton extends StatefulWidget {
|
||||
final VideoPlayerController controller;
|
||||
|
||||
const CustomeMaterialDesktopPlayOrPauseButton({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
CustomeMaterialDesktopPlayOrPauseButtonState createState() =>
|
||||
CustomeMaterialDesktopPlayOrPauseButtonState();
|
||||
}
|
||||
|
||||
class CustomeMaterialDesktopPlayOrPauseButtonState
|
||||
extends State<CustomeMaterialDesktopPlayOrPauseButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final animation = AnimationController(
|
||||
vsync: this,
|
||||
value: widget.controller.value.isPlaying ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (mounted) {
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
widget.controller.addListener(() {
|
||||
if (!mounted) return;
|
||||
final isPlaying = widget.controller.value.isPlaying;
|
||||
if (isPlaying) {
|
||||
animation.forward();
|
||||
} else {
|
||||
animation.reverse();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
animation.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
onPressed: () => widget.controller.value.isPlaying
|
||||
? widget.controller.pause()
|
||||
: widget.controller.play(),
|
||||
iconSize: 25,
|
||||
color: Colors.white,
|
||||
icon: AnimatedIcon(
|
||||
progress: animation,
|
||||
icon: AnimatedIcons.play_pause,
|
||||
size: 25,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// BUTTON: VOLUME
|
||||
|
||||
/// MaterialDesktop design volume button & slider.
|
||||
class CustomMaterialDesktopVolumeButton extends StatefulWidget {
|
||||
final VideoPlayerController controller;
|
||||
|
||||
const CustomMaterialDesktopVolumeButton({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
CustomMaterialDesktopVolumeButtonState createState() =>
|
||||
CustomMaterialDesktopVolumeButtonState();
|
||||
}
|
||||
|
||||
class CustomMaterialDesktopVolumeButtonState
|
||||
extends State<CustomMaterialDesktopVolumeButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late double volume = widget.controller.value.volume;
|
||||
|
||||
StreamSubscription<double>? subscription;
|
||||
|
||||
bool hover = false;
|
||||
|
||||
bool mute = false;
|
||||
double _volume = 0.0;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
widget.controller.addListener(() {
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
volume = widget.controller.value.volume;
|
||||
return MouseRegion(
|
||||
onEnter: (e) {
|
||||
setState(() {
|
||||
hover = true;
|
||||
});
|
||||
},
|
||||
onExit: (e) {
|
||||
setState(() {
|
||||
hover = false;
|
||||
});
|
||||
},
|
||||
child: Listener(
|
||||
onPointerSignal: (event) {
|
||||
if (event is PointerScrollEvent) {
|
||||
if (event.scrollDelta.dy < 0) {
|
||||
widget.controller.setVolume(volume + 0.1);
|
||||
}
|
||||
if (event.scrollDelta.dy > 0) {
|
||||
widget.controller.setVolume(volume - 0.1);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 4.0),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
if (mute) {
|
||||
await widget.controller.setVolume(_volume);
|
||||
mute = !mute;
|
||||
} else if (volume == 0.0) {
|
||||
_volume = 1.0;
|
||||
await widget.controller.setVolume(1.0);
|
||||
mute = false;
|
||||
} else {
|
||||
_volume = volume;
|
||||
await widget.controller.setVolume(0.0);
|
||||
mute = !mute;
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
iconSize: 25,
|
||||
color: Colors.white,
|
||||
icon: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: volume == 0.0
|
||||
? const Icon(
|
||||
Icons.volume_off,
|
||||
key: ValueKey(Icons.volume_off),
|
||||
)
|
||||
: volume < 0.5
|
||||
? const Icon(
|
||||
Icons.volume_down,
|
||||
key: ValueKey(Icons.volume_down),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.volume_up,
|
||||
key: ValueKey(Icons.volume_up),
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedOpacity(
|
||||
opacity: hover ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: AnimatedContainer(
|
||||
width: hover ? (12.0 + 52.0 + 18.0) : 12.0,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 12.0),
|
||||
SizedBox(
|
||||
width: 52.0,
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
trackHeight: 1.2,
|
||||
inactiveTrackColor: const Color(0x3DFFFFFF),
|
||||
activeTrackColor: Colors.white,
|
||||
thumbColor: Colors.white,
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 12 / 2,
|
||||
elevation: 0.0,
|
||||
pressedElevation: 0.0,
|
||||
),
|
||||
trackShape: _CustomTrackShape(),
|
||||
overlayColor: const Color(0x00000000),
|
||||
),
|
||||
child: Slider(
|
||||
value: volume,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
onChanged: (value) async {
|
||||
await widget.controller.setVolume(value);
|
||||
mute = false;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 18.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POSITION INDICATOR
|
||||
|
||||
/// MaterialDesktop design position indicator.
|
||||
class CustomMaterialDesktopPositionIndicator extends StatefulWidget {
|
||||
final VideoPlayerController controller;
|
||||
final Duration? delta;
|
||||
|
||||
const CustomMaterialDesktopPositionIndicator(
|
||||
{super.key, required this.controller, this.delta});
|
||||
|
||||
@override
|
||||
CustomMaterialDesktopPositionIndicatorState createState() =>
|
||||
CustomMaterialDesktopPositionIndicatorState();
|
||||
}
|
||||
|
||||
class CustomMaterialDesktopPositionIndicatorState
|
||||
extends State<CustomMaterialDesktopPositionIndicator> {
|
||||
late Duration position = widget.controller.value.position;
|
||||
late Duration duration = widget.controller.value.duration;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
widget.controller.addListener(() {
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
position = widget.controller.value.position;
|
||||
duration = widget.controller.value.duration;
|
||||
return Text(
|
||||
'${(widget.delta ?? position).label(reference: duration)} / ${duration.label(reference: duration)}',
|
||||
style: const TextStyle(
|
||||
height: 1.0,
|
||||
fontSize: 12.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomTrackShape extends RoundedRectSliderTrackShape {
|
||||
@override
|
||||
Rect getPreferredRect({
|
||||
required RenderBox parentBox,
|
||||
Offset offset = Offset.zero,
|
||||
required SliderThemeData sliderTheme,
|
||||
bool isEnabled = false,
|
||||
bool isDiscrete = false,
|
||||
}) {
|
||||
final height = sliderTheme.trackHeight;
|
||||
final left = offset.dx;
|
||||
final top = offset.dy + (parentBox.size.height - height!) / 2;
|
||||
final width = parentBox.size.width;
|
||||
return Rect.fromLTWH(
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomMaterialDesktopFullscreenButton extends StatefulWidget {
|
||||
final VideoPlayerController controller;
|
||||
final Function(bool) isFullscreen;
|
||||
|
||||
const CustomMaterialDesktopFullscreenButton({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.isFullscreen,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CustomMaterialDesktopFullscreenButton> createState() =>
|
||||
_CustomMaterialDesktopFullscreenButtonState();
|
||||
}
|
||||
|
||||
class _CustomMaterialDesktopFullscreenButtonState
|
||||
extends State<CustomMaterialDesktopFullscreenButton> {
|
||||
bool _isFullscreen = false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
onPressed: () async {
|
||||
final isFullScreen = await setFullScreen();
|
||||
setState(() {
|
||||
_isFullscreen = isFullScreen;
|
||||
});
|
||||
widget.isFullscreen(_isFullscreen);
|
||||
},
|
||||
icon: _isFullscreen
|
||||
? const Icon(Icons.fullscreen_exit)
|
||||
: const Icon(Icons.fullscreen),
|
||||
iconSize: 25,
|
||||
color: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> setFullScreen({bool? value}) async {
|
||||
if (value != null) {
|
||||
final isFullScreen = await windowManager.isFullScreen();
|
||||
if (value != isFullScreen) {
|
||||
await windowManager.setTitleBarStyle(
|
||||
value == false ? TitleBarStyle.normal : TitleBarStyle.hidden);
|
||||
await windowManager.setFullScreen(value);
|
||||
if (value == false) {
|
||||
await windowManager.center();
|
||||
}
|
||||
await windowManager.show();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
final isFullScreen = await windowManager.isFullScreen();
|
||||
if (!isFullScreen) {
|
||||
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
|
||||
await windowManager.setFullScreen(true);
|
||||
await windowManager.show();
|
||||
} else {
|
||||
await windowManager.setTitleBarStyle(TitleBarStyle.normal);
|
||||
await windowManager.setFullScreen(false);
|
||||
await windowManager.center();
|
||||
await windowManager.show();
|
||||
}
|
||||
return isFullScreen;
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class MediaIndicatorBuilder extends StatelessWidget {
|
||||
final bool isVolumeIndicator;
|
||||
final ValueNotifier<double> value;
|
||||
const MediaIndicatorBuilder(
|
||||
{super.key, required this.value, required this.isVolumeIndicator});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: value,
|
||||
builder: (context, value, child) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Row(
|
||||
mainAxisAlignment: isVolumeIndicator
|
||||
? MainAxisAlignment.start
|
||||
: MainAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
width: 30,
|
||||
child: UnconstrainedBox(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
(value * 100).ceil().toString(),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: RotatedBox(
|
||||
quarterTurns: -1,
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
child: SizedBox.fromSize(
|
||||
size: const Size(130, 20),
|
||||
child: LinearProgressIndicator(
|
||||
value: value,
|
||||
backgroundColor: Colors.transparent)),
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
isVolumeIndicator
|
||||
? switch (value) {
|
||||
== 0.0 => Icons.volume_off,
|
||||
< 0.5 => Icons.volume_down,
|
||||
_ => Icons.volume_up,
|
||||
}
|
||||
: switch (value) {
|
||||
< 1.0 / 3.0 => Icons.brightness_low,
|
||||
< 2.0 / 3.0 => Icons.brightness_medium,
|
||||
_ => Icons.brightness_high,
|
||||
},
|
||||
color: Colors.white,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,921 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mangayomi/modules/anime/anime_player_view.dart';
|
||||
import 'package:mangayomi/modules/anime/providers/anime_player_controller_provider.dart';
|
||||
import 'package:mangayomi/modules/anime/fvp/widgets/custom_seekbar.dart';
|
||||
import 'package:mangayomi/modules/anime/fvp/widgets/indicator_builder.dart';
|
||||
import 'package:mangayomi/modules/anime/fvp/widgets/subtitle_view.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/providers/push_router.dart';
|
||||
import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart';
|
||||
import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:volume_controller/volume_controller.dart';
|
||||
import 'package:screen_brightness/screen_brightness.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MobileControllerWidget extends ConsumerStatefulWidget {
|
||||
final AnimeStreamController streamController;
|
||||
final VideoPlayerController videoController;
|
||||
final Widget topButtonBarWidget;
|
||||
final Widget bottomButtonBarWidget;
|
||||
final bool isFullScreen;
|
||||
|
||||
const MobileControllerWidget(
|
||||
{super.key,
|
||||
required this.videoController,
|
||||
required this.topButtonBarWidget,
|
||||
required this.bottomButtonBarWidget,
|
||||
required this.streamController,
|
||||
required this.isFullScreen});
|
||||
|
||||
@override
|
||||
ConsumerState<MobileControllerWidget> createState() =>
|
||||
_MobileControllerWidgetState();
|
||||
}
|
||||
|
||||
class _MobileControllerWidgetState
|
||||
extends ConsumerState<MobileControllerWidget> {
|
||||
bool mount = true;
|
||||
bool visible = true;
|
||||
Duration controlsTransitionDuration = const Duration(milliseconds: 300);
|
||||
Color backdropColor = const Color(0x66000000);
|
||||
Timer? _timer;
|
||||
late final skipDuration =
|
||||
ref.watch(defaultDoubleTapToSkipLengthStateProvider);
|
||||
final ValueNotifier<double> _brightnessValue = ValueNotifier(0.0);
|
||||
final ValueNotifier<bool> _brightnessIndicator = ValueNotifier(false);
|
||||
Timer? _brightnessTimer;
|
||||
|
||||
final ValueNotifier<double> _volumeValue = ValueNotifier(0.0);
|
||||
final ValueNotifier<bool> _volumeIndicator = ValueNotifier(false);
|
||||
Timer? _volumeTimer;
|
||||
// The default event stream in package:volume_controller is buggy.
|
||||
bool _volumeInterceptEventStream = false;
|
||||
|
||||
Offset _dragInitialDelta =
|
||||
Offset.zero; // Initial position for horizontal drag
|
||||
int swipeDuration = 0; // Duration to seek in video
|
||||
bool showSwipeDuration = false; // Whether to show the seek duration overlay
|
||||
|
||||
late bool buffering = widget.videoController.value.isBuffering;
|
||||
final controlsHoverDuration = const Duration(seconds: 3);
|
||||
bool _mountSeekBackwardButton = false;
|
||||
bool _mountSeekForwardButton = false;
|
||||
bool _hideSeekBackwardButton = false;
|
||||
bool _hideSeekForwardButton = false;
|
||||
double buttonBarHeight = 100;
|
||||
final bottomButtonBarMargin = const EdgeInsets.only(left: 16.0, right: 8.0);
|
||||
|
||||
Duration? _seekBarDeltaValueNotifier;
|
||||
|
||||
Offset? _tapPosition;
|
||||
|
||||
void _handleTapDown(TapDownDetails details) {
|
||||
setState(() {
|
||||
_tapPosition = details.localPosition;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (mounted) {
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
||||
|
||||
final horizontalGestureSensitivity = 7500;
|
||||
final verticalGestureSensitivity = 500;
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (!mounted) return;
|
||||
widget.videoController.addListener(
|
||||
() {
|
||||
setState(() {
|
||||
buffering = widget.videoController.value.isBuffering;
|
||||
if (buffering) {
|
||||
_mountSeekBackwardButton = false;
|
||||
_mountSeekForwardButton = false;
|
||||
_hideSeekBackwardButton = false;
|
||||
_hideSeekForwardButton = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Future.microtask(() async {
|
||||
try {
|
||||
await ScreenBrightness().resetScreenBrightness();
|
||||
} catch (_) {}
|
||||
});
|
||||
// --------------------------------------------------
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void onTap() {
|
||||
if (!visible) {
|
||||
setState(() {
|
||||
mount = true;
|
||||
visible = true;
|
||||
});
|
||||
|
||||
_timer?.cancel();
|
||||
_timer = Timer(controlsHoverDuration, () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
visible = false;
|
||||
});
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
visible = false;
|
||||
});
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||
_timer?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
void onDoubleTapSeekBackward() {
|
||||
setState(() {
|
||||
_mountSeekBackwardButton = true;
|
||||
});
|
||||
}
|
||||
|
||||
void onDoubleTapSeekForward() {
|
||||
setState(() {
|
||||
_mountSeekForwardButton = true;
|
||||
});
|
||||
}
|
||||
|
||||
void onHorizontalDragUpdate(DragUpdateDetails details) {
|
||||
if (_dragInitialDelta == Offset.zero) {
|
||||
_dragInitialDelta = details.localPosition;
|
||||
return;
|
||||
}
|
||||
|
||||
final diff = _dragInitialDelta.dx - details.localPosition.dx;
|
||||
final duration = widget.videoController.value.duration.inSeconds;
|
||||
final position = widget.videoController.value.position.inSeconds;
|
||||
|
||||
final seconds = -(diff * duration / horizontalGestureSensitivity).round();
|
||||
final relativePosition = position + seconds;
|
||||
|
||||
if (relativePosition <= duration && relativePosition >= 0) {
|
||||
setState(() {
|
||||
swipeDuration = seconds;
|
||||
showSwipeDuration = true;
|
||||
_seekBarDeltaValueNotifier = Duration(
|
||||
seconds: widget.videoController.value.position.inSeconds + seconds);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void onHorizontalDragEnd() {
|
||||
if (swipeDuration != 0) {
|
||||
Duration newPosition = widget.videoController.value.position +
|
||||
Duration(seconds: swipeDuration);
|
||||
newPosition = newPosition.clamp(
|
||||
Duration.zero,
|
||||
widget.videoController.value.duration,
|
||||
);
|
||||
widget.videoController.seekTo(newPosition);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_dragInitialDelta = Offset.zero;
|
||||
showSwipeDuration = false;
|
||||
_seekBarDeltaValueNotifier = null;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
Future.microtask(() async {
|
||||
try {
|
||||
VolumeController().showSystemUI = false;
|
||||
_volumeValue.value = await VolumeController().getVolume();
|
||||
VolumeController().listener((value) {
|
||||
if (mounted && !_volumeInterceptEventStream) {
|
||||
_volumeValue.value = value;
|
||||
}
|
||||
});
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
Future.microtask(() async {
|
||||
try {
|
||||
_brightnessValue.value = await ScreenBrightness().current;
|
||||
ScreenBrightness().onCurrentBrightnessChanged.listen((value) {
|
||||
if (mounted) {
|
||||
_brightnessValue.value = value;
|
||||
}
|
||||
});
|
||||
} catch (_) {}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> setVolume(double value) async {
|
||||
try {
|
||||
VolumeController().setVolume(value);
|
||||
} catch (_) {}
|
||||
_volumeValue.value = value;
|
||||
_volumeIndicator.value = true;
|
||||
_volumeInterceptEventStream = true;
|
||||
_volumeTimer?.cancel();
|
||||
_volumeTimer = Timer(const Duration(milliseconds: 200), () {
|
||||
if (mounted) {
|
||||
_volumeIndicator.value = false;
|
||||
_volumeInterceptEventStream = false;
|
||||
}
|
||||
});
|
||||
// --------------------------------------------------
|
||||
}
|
||||
|
||||
Future<void> setBrightness(double value) async {
|
||||
try {
|
||||
await ScreenBrightness().setScreenBrightness(value);
|
||||
} catch (_) {}
|
||||
_brightnessIndicator.value = true;
|
||||
_brightnessTimer?.cancel();
|
||||
_brightnessTimer = Timer(const Duration(milliseconds: 200), () {
|
||||
if (mounted) {
|
||||
_brightnessIndicator.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Consumer(
|
||||
builder: (context, ref, _) => Positioned(
|
||||
child: CustomSubtitleView(controller: widget.videoController)),
|
||||
),
|
||||
Focus(
|
||||
autofocus: true,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// // Controls:
|
||||
AnimatedOpacity(
|
||||
curve: Curves.easeInOut,
|
||||
opacity: visible ? 1.0 : 0.0,
|
||||
duration: controlsTransitionDuration,
|
||||
onEnd: () {
|
||||
setState(() {
|
||||
if (!visible) {
|
||||
mount = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: backdropColor,
|
||||
),
|
||||
),
|
||||
// We are adding 16.0 boundary around the actual controls (which contain the vertical drag gesture detectors).
|
||||
// This will make the hit-test on edges (e.g. swiping to: show status-bar, show navigation-bar, go back in navigation) not activate the swipe gesture annoyingly.
|
||||
Positioned.fill(
|
||||
left: 16.0,
|
||||
top: 16.0,
|
||||
right: 16.0,
|
||||
bottom: 16.0,
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
onDoubleTapDown: _handleTapDown,
|
||||
onDoubleTap: () {
|
||||
if (_tapPosition != null &&
|
||||
_tapPosition!.dx >
|
||||
MediaQuery.of(context).size.width / 2) {
|
||||
onDoubleTapSeekForward();
|
||||
} else {
|
||||
onDoubleTapSeekBackward();
|
||||
}
|
||||
},
|
||||
onHorizontalDragUpdate: (details) {
|
||||
onHorizontalDragUpdate(details);
|
||||
},
|
||||
onHorizontalDragEnd: (details) {
|
||||
onHorizontalDragEnd();
|
||||
},
|
||||
onVerticalDragUpdate: (e) async {
|
||||
final delta = e.delta.dy;
|
||||
final Offset position = e.localPosition;
|
||||
|
||||
if (position.dx <=
|
||||
MediaQuery.of(context).size.width / 2) {
|
||||
// Left side of screen swiped
|
||||
|
||||
final brightness = _brightnessValue.value -
|
||||
delta / verticalGestureSensitivity;
|
||||
final result = brightness.clamp(0.0, 1.0);
|
||||
setBrightness(result);
|
||||
} else {
|
||||
// Right side of screen swiped
|
||||
|
||||
final volume = _volumeValue.value -
|
||||
delta / verticalGestureSensitivity;
|
||||
final result = volume.clamp(0.0, 1.0);
|
||||
setVolume(result);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
color: const Color(0x00000000),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (mount)
|
||||
Padding(
|
||||
padding: (
|
||||
// Add padding in fullscreen!
|
||||
widget.isFullScreen
|
||||
? MediaQuery.of(context).padding
|
||||
: EdgeInsets.zero),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
widget.topButtonBarWidget,
|
||||
// Only display [primaryButtonBar] if [buffering] is false.
|
||||
Expanded(
|
||||
child: AnimatedOpacity(
|
||||
curve: Curves.easeInOut,
|
||||
opacity: buffering
|
||||
? 0.0
|
||||
: showSwipeDuration
|
||||
? 0.0
|
||||
: 1.0,
|
||||
duration: controlsTransitionDuration,
|
||||
child: Center(
|
||||
child: Row(
|
||||
children: mobilePrimaryButtonBar(
|
||||
context,
|
||||
widget.isFullScreen,
|
||||
widget.streamController,
|
||||
widget.videoController)),
|
||||
),
|
||||
),
|
||||
),
|
||||
Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: CustomSeekBar(
|
||||
onSeekStart: (value) {
|
||||
setState(() {
|
||||
swipeDuration = value.inSeconds;
|
||||
showSwipeDuration = true;
|
||||
});
|
||||
_timer?.cancel();
|
||||
},
|
||||
onSeekEnd: (value) {
|
||||
_timer = Timer(
|
||||
controlsHoverDuration,
|
||||
() {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
visible = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
setState(() {
|
||||
showSwipeDuration = false;
|
||||
});
|
||||
},
|
||||
controller: widget.videoController,
|
||||
),
|
||||
),
|
||||
widget.bottomButtonBarWidget
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// // Double-Tap Seek Seek-Bar:
|
||||
if (!mount)
|
||||
if (_mountSeekBackwardButton ||
|
||||
_mountSeekForwardButton ||
|
||||
showSwipeDuration)
|
||||
Column(
|
||||
children: [
|
||||
const Spacer(),
|
||||
Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: CustomSeekBar(
|
||||
delta: _seekBarDeltaValueNotifier,
|
||||
controller: widget.videoController),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
// // Buffering Indicator.
|
||||
IgnorePointer(
|
||||
child: Padding(
|
||||
padding: (
|
||||
// Add padding in fullscreen!
|
||||
widget.isFullScreen
|
||||
? MediaQuery.of(context).padding
|
||||
: EdgeInsets.zero),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
height: buttonBarHeight,
|
||||
margin: const EdgeInsets.all(0),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(
|
||||
begin: 0.0,
|
||||
end: buffering ? 1.0 : 0.0,
|
||||
),
|
||||
duration: controlsTransitionDuration,
|
||||
builder: (context, value, child) {
|
||||
// Only mount the buffering indicator if the opacity is greater than 0.0.
|
||||
// This has been done to prevent redundant resource usage in [CircularProgressIndicator].
|
||||
if (value > 0.0) {
|
||||
return Opacity(
|
||||
opacity: value,
|
||||
child: child!,
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
child: const CircularProgressIndicator(
|
||||
color: Color(0xFFFFFFFF),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: buttonBarHeight,
|
||||
margin: bottomButtonBarMargin,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// // Volume Indicator.
|
||||
IgnorePointer(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _volumeIndicator,
|
||||
builder: (context, value, child) => AnimatedOpacity(
|
||||
curve: Curves.easeInOut,
|
||||
opacity: value ? 1.0 : 0.0,
|
||||
duration: controlsTransitionDuration,
|
||||
child: MediaIndicatorBuilder(
|
||||
value: _volumeValue, isVolumeIndicator: true)),
|
||||
),
|
||||
),
|
||||
// // Brightness Indicator.
|
||||
IgnorePointer(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _brightnessIndicator,
|
||||
builder: (context, value, child) => AnimatedOpacity(
|
||||
curve: Curves.easeInOut,
|
||||
opacity: value ? 1.0 : 0.0,
|
||||
duration: controlsTransitionDuration,
|
||||
child: MediaIndicatorBuilder(
|
||||
value: _brightnessValue, isVolumeIndicator: false)),
|
||||
),
|
||||
),
|
||||
// Seek Indicator.
|
||||
IgnorePointer(
|
||||
child: AnimatedOpacity(
|
||||
duration: controlsTransitionDuration,
|
||||
opacity: showSwipeDuration ? 1 : 0,
|
||||
child: seekIndicatorTextWidget(
|
||||
Duration(seconds: swipeDuration),
|
||||
widget.videoController.value.position)),
|
||||
),
|
||||
|
||||
// Double-Tap Seek Button(s):
|
||||
if (_mountSeekBackwardButton || _mountSeekForwardButton)
|
||||
Positioned.fill(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _mountSeekBackwardButton
|
||||
? TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(
|
||||
begin: 0.0,
|
||||
end: _hideSeekBackwardButton ? 0.0 : 1.0,
|
||||
),
|
||||
duration: const Duration(milliseconds: 200),
|
||||
builder: (context, value, child) => Opacity(
|
||||
opacity: value,
|
||||
child: child,
|
||||
),
|
||||
onEnd: () {
|
||||
if (_hideSeekBackwardButton) {
|
||||
setState(() {
|
||||
_hideSeekBackwardButton = false;
|
||||
_mountSeekBackwardButton = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: _BackwardSeekIndicator(
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_seekBarDeltaValueNotifier = widget
|
||||
.videoController
|
||||
.value
|
||||
.position -
|
||||
value;
|
||||
});
|
||||
},
|
||||
onSubmitted: (value) {
|
||||
setState(() {
|
||||
_hideSeekBackwardButton = true;
|
||||
});
|
||||
var result = widget
|
||||
.videoController.value.position -
|
||||
value;
|
||||
result = result.clamp(
|
||||
Duration.zero,
|
||||
widget.videoController.value.duration,
|
||||
);
|
||||
widget.videoController.seekTo(result);
|
||||
},
|
||||
skipDuration: skipDuration),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
Expanded(
|
||||
child: _mountSeekForwardButton
|
||||
? TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(
|
||||
begin: 0.0,
|
||||
end: _hideSeekForwardButton ? 0.0 : 1.0,
|
||||
),
|
||||
duration: const Duration(milliseconds: 200),
|
||||
builder: (context, value, child) => Opacity(
|
||||
opacity: value,
|
||||
child: child,
|
||||
),
|
||||
onEnd: () {
|
||||
if (_hideSeekForwardButton) {
|
||||
setState(() {
|
||||
_hideSeekForwardButton = false;
|
||||
_mountSeekForwardButton = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: _ForwardSeekIndicator(
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_seekBarDeltaValueNotifier = widget
|
||||
.videoController
|
||||
.value
|
||||
.position +
|
||||
value;
|
||||
});
|
||||
},
|
||||
onSubmitted: (value) {
|
||||
setState(() {
|
||||
_hideSeekForwardButton = true;
|
||||
});
|
||||
var result = widget
|
||||
.videoController.value.position +
|
||||
value;
|
||||
result = result.clamp(
|
||||
Duration.zero,
|
||||
widget.videoController.value.duration,
|
||||
);
|
||||
widget.videoController.seekTo(result);
|
||||
},
|
||||
skipDuration: skipDuration),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BackwardSeekIndicator extends StatefulWidget {
|
||||
final void Function(Duration) onChanged;
|
||||
final void Function(Duration) onSubmitted;
|
||||
final int skipDuration;
|
||||
const _BackwardSeekIndicator({
|
||||
required this.onChanged,
|
||||
required this.onSubmitted,
|
||||
required this.skipDuration,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_BackwardSeekIndicator> createState() => _BackwardSeekIndicatorState();
|
||||
}
|
||||
|
||||
class _BackwardSeekIndicatorState extends State<_BackwardSeekIndicator> {
|
||||
late Duration value = Duration(seconds: widget.skipDuration);
|
||||
|
||||
Timer? timer;
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (mounted) {
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
timer = Timer(const Duration(milliseconds: 400), () {
|
||||
widget.onSubmitted.call(value);
|
||||
});
|
||||
}
|
||||
|
||||
void increment() {
|
||||
timer?.cancel();
|
||||
timer = Timer(const Duration(milliseconds: 400), () {
|
||||
widget.onSubmitted.call(value);
|
||||
});
|
||||
widget.onChanged.call(value);
|
||||
setState(() {
|
||||
value += Duration(seconds: widget.skipDuration);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Color(0x88767676),
|
||||
Color(0x00767676),
|
||||
],
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
splashColor: const Color(0x44767676),
|
||||
onTap: increment,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.fast_rewind,
|
||||
size: 24.0,
|
||||
color: Color(0xFFFFFFFF),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
'${value.inSeconds} seconds',
|
||||
style: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: Color(0xFFFFFFFF),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ForwardSeekIndicator extends StatefulWidget {
|
||||
final void Function(Duration) onChanged;
|
||||
final void Function(Duration) onSubmitted;
|
||||
final int skipDuration;
|
||||
const _ForwardSeekIndicator({
|
||||
required this.onChanged,
|
||||
required this.onSubmitted,
|
||||
required this.skipDuration,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_ForwardSeekIndicator> createState() => _ForwardSeekIndicatorState();
|
||||
}
|
||||
|
||||
class _ForwardSeekIndicatorState extends State<_ForwardSeekIndicator> {
|
||||
late Duration value = Duration(seconds: widget.skipDuration);
|
||||
|
||||
Timer? timer;
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (mounted) {
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
timer = Timer(const Duration(milliseconds: 400), () {
|
||||
widget.onSubmitted.call(value);
|
||||
});
|
||||
}
|
||||
|
||||
void increment() {
|
||||
timer?.cancel();
|
||||
timer = Timer(const Duration(milliseconds: 400), () {
|
||||
widget.onSubmitted.call(value);
|
||||
});
|
||||
widget.onChanged.call(value);
|
||||
setState(() {
|
||||
value += Duration(seconds: widget.skipDuration);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Color(0x00767676),
|
||||
Color(0x88767676),
|
||||
],
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
splashColor: const Color(0x44767676),
|
||||
onTap: increment,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.fast_forward,
|
||||
size: 24.0,
|
||||
color: Color(0xFFFFFFFF),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
'${value.inSeconds} seconds',
|
||||
style: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: Color(0xFFFFFFFF),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// BUTTON: PLAY/PAUSE
|
||||
|
||||
/// A material design play/pause button.
|
||||
class CustomMaterialPlayOrPauseButton extends StatefulWidget {
|
||||
final VideoPlayerController controller;
|
||||
|
||||
const CustomMaterialPlayOrPauseButton({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
CustomMaterialPlayOrPauseButtonState createState() =>
|
||||
CustomMaterialPlayOrPauseButtonState();
|
||||
}
|
||||
|
||||
class CustomMaterialPlayOrPauseButtonState
|
||||
extends State<CustomMaterialPlayOrPauseButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final animation = AnimationController(
|
||||
vsync: this,
|
||||
value: widget.controller.value.isPlaying ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (mounted) {
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
widget.controller.addListener(() {
|
||||
if (!mounted) return;
|
||||
final isPlaying = widget.controller.value.isPlaying;
|
||||
if (isPlaying) {
|
||||
animation.forward();
|
||||
} else {
|
||||
animation.reverse();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
animation.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
onPressed: () => widget.controller.value.isPlaying
|
||||
? widget.controller.pause()
|
||||
: widget.controller.play(),
|
||||
iconSize: 65,
|
||||
color: Colors.white,
|
||||
icon: IgnorePointer(
|
||||
child: AnimatedIcon(
|
||||
progress: animation,
|
||||
icon: AnimatedIcons.play_pause,
|
||||
size: 65,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> mobilePrimaryButtonBar(BuildContext context, bool isFullScreen,
|
||||
AnimeStreamController streamController, VideoPlayerController controller) {
|
||||
bool hasPrevEpisode = streamController.getEpisodeIndex().$1 + 1 !=
|
||||
streamController.getEpisodesLength(streamController.getEpisodeIndex().$2);
|
||||
bool hasNextEpisode = streamController.getEpisodeIndex().$1 != 0;
|
||||
|
||||
return [
|
||||
const Spacer(flex: 3),
|
||||
IconButton(
|
||||
onPressed: hasPrevEpisode
|
||||
? () {
|
||||
if (isFullScreen) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
}
|
||||
pushReplacementMangaReaderView(
|
||||
context: context, chapter: streamController.getPrevEpisode());
|
||||
}
|
||||
: null,
|
||||
icon: Icon(
|
||||
Icons.skip_previous,
|
||||
size: 35,
|
||||
color: hasPrevEpisode ? Colors.white : Colors.grey,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
CustomMaterialPlayOrPauseButton(controller: controller),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: hasNextEpisode
|
||||
? () {
|
||||
if (isFullScreen) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
}
|
||||
pushReplacementMangaReaderView(
|
||||
context: context,
|
||||
chapter: streamController.getNextEpisode(),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
icon: Icon(Icons.skip_next,
|
||||
size: 35, color: hasPrevEpisode ? Colors.white : Colors.grey),
|
||||
),
|
||||
const Spacer(flex: 3)
|
||||
];
|
||||
}
|
||||
// SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
// ignore_for_file: deprecated_member_use
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mangayomi/modules/anime/providers/state_provider.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
class CustomSubtitleView extends ConsumerStatefulWidget {
|
||||
final VideoPlayerController controller;
|
||||
|
||||
const CustomSubtitleView({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
ConsumerState<CustomSubtitleView> createState() => _CustomSubtitleViewState();
|
||||
}
|
||||
|
||||
class _CustomSubtitleViewState extends ConsumerState<CustomSubtitleView> {
|
||||
late String subtitle = widget.controller.value.caption.text;
|
||||
late Duration duration = const Duration(milliseconds: 100);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
widget.controller.addListener(() {
|
||||
setState(() {
|
||||
subtitle = widget.controller.value.caption.text;
|
||||
});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final textScaleFactor = MediaQuery.of(context).textScaleFactor *
|
||||
sqrt(
|
||||
((constraints.maxWidth * constraints.maxHeight) /
|
||||
(1920.0 * 1080.0))
|
||||
.clamp(0.0, 1.0),
|
||||
);
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: AnimatedContainer(
|
||||
duration: duration,
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Text(
|
||||
subtitle,
|
||||
style: subtileTextStyle(ref),
|
||||
textAlign: TextAlign.center,
|
||||
textScaleFactor: textScaleFactor,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TextStyle subtileTextStyle(WidgetRef ref) {
|
||||
final subSets = ref.watch(subtitleSettingsStateProvider);
|
||||
final borderColor = Color.fromARGB(subSets.borderColorA!,
|
||||
subSets.borderColorR!, subSets.borderColorG!, subSets.borderColorB!);
|
||||
return TextStyle(
|
||||
fontSize: subSets.fontSize!.toDouble(),
|
||||
fontWeight: subSets.useBold! ? FontWeight.bold : null,
|
||||
fontStyle: subSets.useItalic! ? FontStyle.italic : null,
|
||||
color: Color.fromARGB(subSets.textColorA!, subSets.textColorR!,
|
||||
subSets.textColorG!, subSets.textColorB!),
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: const Offset(-1.5, -1.5),
|
||||
color: borderColor,
|
||||
blurRadius: 1.4),
|
||||
Shadow(
|
||||
offset: const Offset(1.5, -1.5),
|
||||
color: borderColor,
|
||||
blurRadius: 1.4),
|
||||
Shadow(
|
||||
offset: const Offset(1.5, 1.5),
|
||||
color: borderColor,
|
||||
blurRadius: 1.4),
|
||||
Shadow(
|
||||
offset: const Offset(-1.5, 1.5),
|
||||
color: borderColor,
|
||||
blurRadius: 1.4)
|
||||
],
|
||||
backgroundColor: Color.fromARGB(
|
||||
subSets.backgroundColorA!,
|
||||
subSets.backgroundColorR!,
|
||||
subSets.backgroundColorG!,
|
||||
subSets.backgroundColorB!));
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@ import 'package:mangayomi/models/chapter.dart';
|
|||
import 'package:mangayomi/models/source.dart';
|
||||
import 'package:mangayomi/models/track_preference.dart';
|
||||
import 'package:mangayomi/modules/anime/anime_player_view.dart';
|
||||
import 'package:mangayomi/modules/anime/fvp/anime_player_view.dart';
|
||||
import 'package:mangayomi/modules/browse/extension/edit_code.dart';
|
||||
import 'package:mangayomi/modules/browse/extension/extension_detail.dart';
|
||||
import 'package:mangayomi/modules/browse/extension/widgets/create_extension.dart';
|
||||
|
|
@ -205,17 +204,17 @@ class RouterNotifier extends ChangeNotifier {
|
|||
name: "animePlayerView",
|
||||
builder: (context, state) {
|
||||
final episode = state.extra as Chapter;
|
||||
return Platform.isWindows
|
||||
? AnimePlayerViewFvp(episode: episode)
|
||||
: AnimePlayerView(episode: episode);
|
||||
return AnimePlayerView(
|
||||
episode: episode,
|
||||
);
|
||||
},
|
||||
pageBuilder: (context, state) {
|
||||
final episode = state.extra as Chapter;
|
||||
return transitionPage(
|
||||
key: state.pageKey,
|
||||
child: Platform.isWindows
|
||||
? AnimePlayerViewFvp(episode: episode)
|
||||
: AnimePlayerView(episode: episode),
|
||||
child: AnimePlayerView(
|
||||
episode: episode,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <flutter_qjs/flutter_qjs_plugin.h>
|
||||
#include <fvp/fvp_plugin.h>
|
||||
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
|
||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||
#include <media_kit_video/media_kit_video_plugin.h>
|
||||
|
|
@ -20,9 +19,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||
g_autoptr(FlPluginRegistrar) flutter_qjs_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterQjsPlugin");
|
||||
flutter_qjs_plugin_register_with_registrar(flutter_qjs_registrar);
|
||||
g_autoptr(FlPluginRegistrar) fvp_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FvpPlugin");
|
||||
fvp_plugin_register_with_registrar(fvp_registrar);
|
||||
g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin");
|
||||
isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
flutter_qjs
|
||||
fvp
|
||||
isar_flutter_libs
|
||||
media_kit_libs_linux
|
||||
media_kit_video
|
||||
|
|
@ -16,6 +15,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
jni
|
||||
media_kit_native_event_loop
|
||||
rust_lib_mangayomi
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import Foundation
|
|||
import desktop_webview_window
|
||||
import flutter_qjs
|
||||
import flutter_web_auth_2
|
||||
import fvp
|
||||
import isar_flutter_libs
|
||||
import media_kit_libs_macos_video
|
||||
import media_kit_video
|
||||
|
|
@ -18,7 +17,6 @@ import screen_brightness_macos
|
|||
import screen_retriever
|
||||
import share_plus
|
||||
import url_launcher_macos
|
||||
import video_player_avfoundation
|
||||
import wakelock_plus
|
||||
import window_manager
|
||||
import window_to_front
|
||||
|
|
@ -27,7 +25,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||
DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin"))
|
||||
FlutterQjsPlugin.register(with: registry.registrar(forPlugin: "FlutterQjsPlugin"))
|
||||
FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin"))
|
||||
FvpPlugin.register(with: registry.registrar(forPlugin: "FvpPlugin"))
|
||||
IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin"))
|
||||
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
||||
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
|
||||
|
|
@ -37,7 +34,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
|
||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
||||
WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin"))
|
||||
|
|
|
|||
17
packages/media_kit/.gitignore
vendored
17
packages/media_kit/.gitignore
vendored
|
|
@ -1,17 +0,0 @@
|
|||
# Files and directories created by pub.
|
||||
.dart_tool/
|
||||
.packages
|
||||
|
||||
# Conventional directory for build output.
|
||||
build/
|
||||
|
||||
# libMPV headers.
|
||||
|
||||
headers/*
|
||||
|
||||
# Others
|
||||
.vscode
|
||||
cover.jpg
|
||||
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
|
|
@ -1,303 +0,0 @@
|
|||
## 1.1.10+1
|
||||
|
||||
- docs: document updated `media_kit_video`
|
||||
|
||||
## 1.1.10
|
||||
|
||||
- feat: prevent multiple calls to `MediaKit.ensureInitialized`
|
||||
|
||||
## 1.1.9
|
||||
|
||||
- fix: `NativePlayer._command`
|
||||
- fix: `NativePlayer` play after completed
|
||||
- fix(web): `AssetLoader` in release mode
|
||||
- chore: reduce demuxer cache size to 32MB
|
||||
- feat: `PlayerConfiguration.muted` for `NativePlayer` & `WebPlayer`
|
||||
|
||||
## 1.1.8+2
|
||||
|
||||
- docs: document updated `media_kit_video`
|
||||
|
||||
## 1.1.8+1
|
||||
|
||||
- docs: document updated `media_kit_video`, `media_kit_libs_video` & `media_kit_libs_audio`
|
||||
|
||||
## 1.1.8
|
||||
|
||||
- fix: reset subtitle state/stream in `Player.setSubtitleTrack`
|
||||
- fix: explicit comparator in `PlayerStream` `Stream.distinct`
|
||||
- fix: add `rtp` & `udp` to `protocol_whitelist` by default
|
||||
- fix: reset `Player` state/stream upon `Player.open`
|
||||
- fix: `Player.stop` memory leak
|
||||
- fix(web): do not throw `UnsupportedError` w/ `SubtitleTrack.(auto|no)`
|
||||
- perf: move `NativePlayer.seek` to `Isolate`
|
||||
- build: update `package:uuid` version constraint
|
||||
|
||||
## 1.1.7
|
||||
|
||||
- fix: close `PlatformPlayer.playlistModeController`
|
||||
|
||||
## 1.1.6
|
||||
|
||||
- feat: HLS support for web
|
||||
- fix: movtext subtitles not working
|
||||
- fix: expose composition model classes
|
||||
- fix: increase default demuxer cache size to 128 MB
|
||||
- fix(android): S/W rendering fallback
|
||||
- fix(android): create fresh `android.view.Surface` for every video output
|
||||
|
||||
## 1.1.5
|
||||
|
||||
- feat: `Media.memory`
|
||||
|
||||
## 1.1.4+1
|
||||
|
||||
- docs: document updated
|
||||
- `media_kit_video`
|
||||
|
||||
## 1.1.4
|
||||
|
||||
- feat: `VideoTrack`/`AudioTrack`/`SubtitleTrack` expose more parameters
|
||||
- feat: `NativePlayer.observeProperty` & `NativePlayer.unobserveProperty`
|
||||
- fix: `error` stream not being emitted in certain cases
|
||||
- fix: pause during buffering makes `Player` not exit buffering
|
||||
- fix: DASH having `BaseURL`(s) with special characters not loading
|
||||
- fix(windows/android): AV1 support
|
||||
|
||||
## 1.1.3+1
|
||||
|
||||
- docs: document updated
|
||||
- `media_kit_libs_ios_audio`
|
||||
- `media_kit_libs_ios_video`
|
||||
- `media_kit_libs_macos_audio`
|
||||
- `media_kit_libs_macos_video`
|
||||
|
||||
## 1.1.3
|
||||
|
||||
- fix: `EXT-X-KEY` support
|
||||
- fix: set `subs-fallback` & `subs-with-matching-audio`
|
||||
- fix(android): use `hwdec=auto`
|
||||
- fix(android): `SurfaceTexture.setDefaultBufferSize` & render race
|
||||
|
||||
## 1.1.2+1
|
||||
|
||||
- docs: document updated
|
||||
- `media_kit_libs_ios_audio`
|
||||
- `media_kit_libs_ios_video`
|
||||
- `media_kit_libs_macos_audio`
|
||||
- `media_kit_libs_macos_video`
|
||||
|
||||
## 1.1.2
|
||||
|
||||
- feat: export `PlayerState` & `PlayerStream`
|
||||
|
||||
## 1.1.1
|
||||
|
||||
- feat: `Player.screenshot` pixel-buffer support
|
||||
- feat: WebVTT over HLS support
|
||||
- feat(android): DASH support
|
||||
- feat(android): load/call `mpv_lavc_set_java_vm` in `AndroidHelper`
|
||||
- perf(android): static link FFmpeg w/ libmpv
|
||||
- perf: reduce bundle size
|
||||
- perf: improve `Player.dispose` & eliminate `mpv_terminate_destroy` delay
|
||||
- fix(windows): broken HLS support
|
||||
|
||||
## 1.1.0
|
||||
|
||||
- feat: `Player.screenshot` for capturing video snapshots as `Uint8List`
|
||||
- feat: external audio track & subtitle track support
|
||||
- `AudioTrack.uri`
|
||||
- `SubtitleTrack.uri` & `SubtitleTrack.data`
|
||||
- feat: WebVTT subtitle support
|
||||
- feat: `Player.state.videoParams` & `Player.stream.videoParams`
|
||||
- feat: `Player.state.subtitle` & `Player.stream.subtitle`
|
||||
- perf(android): use `hwdec=mediacodec` w/ `enableHardwareAcceleration`
|
||||
- fix(android): OpenSL ES limit
|
||||
- fix(android): improve stability
|
||||
- fix(android): file-descriptor clean-up for content:// URI
|
||||
- fix(windows): improve stability
|
||||
- fix: immediately set `vid`/`aid`/`sid` to `no` in `dispose`
|
||||
- perf: reduce bundle size by <= 50%
|
||||
- perf: do not decode video until `VideoController` attach
|
||||
|
||||
## 1.0.2
|
||||
|
||||
- deps: update [`package:http`](https://pub.dev/packages/http) dependency constraint
|
||||
|
||||
## 1.0.1
|
||||
|
||||
- deps: bump [`package:http`](https://pub.dev/packages/http) to `1.1.0`
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- feat: web support
|
||||
- feat: `Player.stop`
|
||||
- feat: support for AGP 8.0
|
||||
- feat: pre-built video controls
|
||||
- fix: `buffering` stream behavior
|
||||
- fix: improve stability on Android emulator(s)
|
||||
- fix: default `PlayerState` `volume` = `100.0`
|
||||
- fix: `Player.add`, `Player.remove`, `Player.jump` & `Player.move` stability
|
||||
- test: stricter & more unit-tests
|
||||
|
||||
## 0.0.11
|
||||
|
||||
- fix: `audioDevices` state/stream not being set/emit
|
||||
|
||||
## 0.0.10+1
|
||||
|
||||
- docs: update demo application links
|
||||
|
||||
## 0.0.10
|
||||
|
||||
- perf: emit distinct events in `Player.stream`
|
||||
- fix(android): crash on some devices
|
||||
- fix: `Player.setAudioDevice` not working
|
||||
- fix: set/emit `completed` as `false` upon `Player.seek`
|
||||
|
||||
## 0.0.9+1
|
||||
|
||||
- docs: document updated `media_kit_video`
|
||||
|
||||
## 0.0.9
|
||||
|
||||
- fix(android): crash on Android 6.0 or lower
|
||||
|
||||
## 0.0.8
|
||||
|
||||
- fix: `Player.dispose` event loop clean-up
|
||||
- refactor: `Player` implementation clean-up
|
||||
- feat: `Initializer.dispose`
|
||||
- feat: `InitializerIsolate.dispose`
|
||||
- feat: `InitializerNativeEventLoop.dispose`
|
||||
- feat: `PlatformPlayer.waitForVideoControllerInitializationIfAttached`
|
||||
- feat: HTTP headers support in `Media`
|
||||
|
||||
## 0.0.7+1
|
||||
|
||||
- docs: document updated `media_kit_libs_android_video` and `media_kit_libs_android_audio`
|
||||
|
||||
## 0.0.7
|
||||
|
||||
- fix: `MediaKit.ensureInitialized` not passing optional `libmpv` argument
|
||||
|
||||
## 0.0.6
|
||||
|
||||
- feat: synchronize `Player` methods
|
||||
- refactor: improve `Playlist` handling in `Player`
|
||||
- refactor: improve handling of `playlist`, `audioBitrate` & `audioParams` states/events
|
||||
|
||||
## 0.0.5+2
|
||||
|
||||
- docs: document updated `media_kit_video` & `media_kit_libs_windows_audio`
|
||||
|
||||
## 0.0.5+1
|
||||
|
||||
- docs: document updated `media_kit_video`
|
||||
|
||||
## 0.0.5
|
||||
|
||||
- Android support
|
||||
- feat: video output width & height states/events:
|
||||
- `Player.state.width`: currently playing video's width as `int`
|
||||
- `Player.stream.width`: currently playing video's width as `Stream<int>`
|
||||
- `Player.state.height`: currently playing video's height as `int`
|
||||
- `Player.stream.height`: currently playing video's height as `Stream<int>`
|
||||
- feat(refactor): entry point
|
||||
- `MediaKit.ensureInitialized`
|
||||
- feat: media stream buffer state/event:
|
||||
- `Player.state.buffer`: currently buffered duration of the media stream as `Duration`
|
||||
- `Player.stream.buffer`: currently buffered duration of the media stream as `Stream<Duration>`
|
||||
- perf: limit demuxer cache size to 32 MB by default
|
||||
- fix: HTTPS m3u8 file loading
|
||||
- fix: asset names with special characters
|
||||
- feat: `protocolWhitelist` in `PlayerConfiguration` for whitelisting protocols
|
||||
- feat: `bufferSize` in `PlayerConfiguration` for setting demuxer cache size
|
||||
|
||||
## 0.0.4+1
|
||||
|
||||
- docs(fix): images on pub.dev
|
||||
|
||||
## 0.0.4
|
||||
|
||||
- fix: opening `Playlist` (with `index` > 0) causes index to be treated 0 after internal queue was finished
|
||||
- fix: double `play` calls making `Player` paused
|
||||
|
||||
## 0.0.3+3
|
||||
|
||||
- docs: document updated `media_kit_video`, `media_kit_libs_macos_video` and `media_kit_libs_ios_video`
|
||||
|
||||
## 0.0.3+2
|
||||
|
||||
- docs: document updated `media_kit_video`
|
||||
|
||||
## 0.0.3+1
|
||||
|
||||
- docs: document updated `media_kit_native_event_loop`
|
||||
|
||||
## 0.0.3
|
||||
|
||||
- fix: unable to publish iOS to AppStore
|
||||
- fix: support for iOS simulator
|
||||
|
||||
## 0.0.2
|
||||
|
||||
- macOS support
|
||||
- iOS support
|
||||
- feat: draw first frame upon `Player.open` before `Player.play` (#69)
|
||||
- feat: `Player.open` now accepts `Playable` i.e. `Media` or `Playlist`
|
||||
- feat: access `Player` logs from internal backend e.g. libmpv
|
||||
- `PlayerLogs`: class
|
||||
- `Player.stream.logs`: logs as `Stream<PlayerLogs>`
|
||||
- fix: improve internal playlist handling & management
|
||||
- feat: audio output device selection & enumeration
|
||||
- `Player.setAudioDevice`: method
|
||||
- `AudioDevice`: class
|
||||
- `AudioDevice.auto`: factory constructor
|
||||
- `Player.state.audioDevice`: currently selected audio device as `AudioDevice`
|
||||
- `Player.stream.audioDevice`: currently selected audio device as `Stream<AudioDevice>`
|
||||
- `Player.state.audioDevices`: currently available audio device(s) as `List<AudioDevice>`
|
||||
- `Player.stream.audioDevices`: currently available audio device(s) as `Stream<List<AudioDevice>>`
|
||||
- feat: video, audio & subtitle track selection & enumeration (#54)
|
||||
- `Player.selectVideoTrack`: method
|
||||
- `Player.selectAudioTrack`: method
|
||||
- `Player.selectSubtitleTrack`: method
|
||||
- `VideoTrack`: class
|
||||
- `AudioTrack`: class
|
||||
- `SubtitleTrack`: class
|
||||
- `VideoTrack.auto`: factory constructor
|
||||
- `VideoTrack.no`: factory constructor
|
||||
- `AudioTrack.auto`: factory constructor
|
||||
- `AudioTrack.no`: factory constructor
|
||||
- `SubtitleTrack.auto`: factory constructor
|
||||
- `SubtitleTrack.no`: factory constructor
|
||||
- `Player.state.track.video`: currently selected video track as `VideoTrack`
|
||||
- `Player.stream.track.video`: currently selected video track as `Stream<VideoTrack>`
|
||||
- `Player.state.track.audio`: currently selected audio track as `AudioTrack`
|
||||
- `Player.stream.track.audio`: currently selected audio track as `Stream<AudioTrack>`
|
||||
- `Player.state.track.subtitle`: currently selected subtitle track as `SubtitleTrack`
|
||||
- `Player.stream.track.subtitle`: currently selected subtitle track as `Stream<SubtitleTrack>`
|
||||
- `Player.state.tracks.video`: currently available video track(s) as `List<VideoTrack>`
|
||||
- `Player.stream.tracks.video`: currently available video track(s) as `Stream<List<VideoTrack>>`
|
||||
- `Player.state.tracks.audio`: currently available audio track(s) as `List<AudioTrack>`
|
||||
- `Player.stream.tracks.audio`: currently available audio track(s) as `Stream<List<AudioTrack>>`
|
||||
- `Player.state.tracks.subtitle`: currently available subtitle track(s) as `List<SubtitleTrack>`
|
||||
- `Player.stream.tracks.subtitle`: currently available subtitle track(s) as `Stream<List<SubtitleTrack>>`
|
||||
- refactor: rename `Player.volume` setter to `Player.setVolume`
|
||||
- refactor: rename `Player.rate` setter to `Player.setRate`
|
||||
- refactor: rename `Player.pitch` setter to `Player.setPitch`
|
||||
- refactor: rename `Player.shuffle` setter to `Player.setShuffle`
|
||||
- refactor: rename `Player.state.isPlaying` to `Player.state.playing`
|
||||
- refactor: rename `Player.state.isPaused` to `Player.state.paused`
|
||||
- refactor: rename `Player.state.isCompleted` to `Player.state.completed`
|
||||
- refactor: rename `Player.state.isBuffering` to `Player.state.buffering`
|
||||
- refactor: rename `Player.stream.isPlaying` to `Player.stream.playing`
|
||||
- refactor: rename `Player.stream.isPaused` to `Player.stream.paused`
|
||||
- refactor: rename `Player.stream.isCompleted` to `Player.stream.completed`
|
||||
- refactor: rename `Player.stream.isBuffering` to `Player.stream.buffering`
|
||||
|
||||
## 0.0.1
|
||||
|
||||
- Microsoft Windows support
|
||||
- GNU/Linux support
|
||||
- Initial release
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 & onwards Hitesh Kumar Saini <saini123hitesh@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,30 +0,0 @@
|
|||
# This file configures the static analysis results for your project (errors,
|
||||
# warnings, and lints).
|
||||
#
|
||||
# This enables the 'recommended' set of lints from `package:lints`.
|
||||
# This set helps identify many issues that may lead to problems when running
|
||||
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
|
||||
# style and format.
|
||||
#
|
||||
# If you want a smaller set of lints you can change this to specify
|
||||
# 'package:lints/core.yaml'. These are just the most critical lints
|
||||
# (the recommended set includes the core lints).
|
||||
# The core lints are also what is used by pub.dev for scoring packages.
|
||||
|
||||
include: package:lints/recommended.yaml
|
||||
|
||||
# Uncomment the following section to specify additional rules.
|
||||
|
||||
# linter:
|
||||
# rules:
|
||||
# - camel_case_types
|
||||
|
||||
# analyzer:
|
||||
# exclude:
|
||||
# - path/to/excluded/files/**
|
||||
|
||||
# For more information about the core and recommended set of lints, see
|
||||
# https://dart.dev/go/core-lints
|
||||
|
||||
# For additional information about configuring this file, see
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,59 +0,0 @@
|
|||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// Make sure to add following packages to pubspec.yaml:
|
||||
// * media_kit
|
||||
// * media_kit_video
|
||||
// * media_kit_libs_video
|
||||
import 'package:media_kit/media_kit.dart'; // Provides [Player], [Media], [Playlist] etc.
|
||||
import 'package:media_kit_video/media_kit_video.dart'; // Provides [VideoController] & [Video] etc.
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
// Necessary initialization for package:media_kit.
|
||||
MediaKit.ensureInitialized();
|
||||
runApp(
|
||||
const MaterialApp(
|
||||
home: MyScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class MyScreen extends StatefulWidget {
|
||||
const MyScreen({Key? key}) : super(key: key);
|
||||
@override
|
||||
State<MyScreen> createState() => MyScreenState();
|
||||
}
|
||||
|
||||
class MyScreenState extends State<MyScreen> {
|
||||
// Create a [Player] to control playback.
|
||||
late final player = Player();
|
||||
// Create a [VideoController] to handle video output from [Player].
|
||||
late final controller = VideoController(player);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Play a [Media] or [Playlist].
|
||||
player.open(Media('https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4'));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
player.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: MediaQuery.of(context).size.width * 9.0 / 16.0,
|
||||
// Use [Video] widget to display video output.
|
||||
child: Video(controller: controller),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package:media_kit bundles package:ffi v1.2.1 for internal native interop.
|
||||
|
||||
This has been done to improve stability & performance on Microsoft Windows.
|
||||
PR: https://github.com/dart-lang/ffi/pull/144 seems to make some changes to how memory is allocated by FFI on Microsoft Windows.
|
||||
Even though it works fine in most cases, there are certain situations where notable differences in stability or performance are observed (crash or slow processing).
|
||||
|
||||
I don't have much knowledge of Microsoft Windows's internals. However, it seems that memory allocated by new implementation has some sort of incompatibility with MinGW compiled libmpv.
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
export 'src/allocation.dart' show calloc, malloc;
|
||||
export 'src/arena.dart';
|
||||
export 'src/utf8.dart';
|
||||
export 'src/utf16.dart';
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
|
||||
// Note that kernel32.dll is the correct name in both 32-bit and 64-bit.
|
||||
final DynamicLibrary stdlib = Platform.isWindows
|
||||
? DynamicLibrary.open('kernel32.dll')
|
||||
: DynamicLibrary.process();
|
||||
|
||||
typedef PosixMallocNative = Pointer Function(IntPtr);
|
||||
typedef PosixMalloc = Pointer Function(int);
|
||||
final PosixMalloc posixMalloc =
|
||||
stdlib.lookupFunction<PosixMallocNative, PosixMalloc>('malloc');
|
||||
|
||||
typedef PosixCallocNative = Pointer Function(IntPtr num, IntPtr size);
|
||||
typedef PosixCalloc = Pointer Function(int num, int size);
|
||||
final PosixCalloc posixCalloc =
|
||||
stdlib.lookupFunction<PosixCallocNative, PosixCalloc>('calloc');
|
||||
|
||||
typedef PosixFreeNative = Void Function(Pointer);
|
||||
typedef PosixFree = void Function(Pointer);
|
||||
final PosixFree posixFree =
|
||||
stdlib.lookupFunction<PosixFreeNative, PosixFree>('free');
|
||||
|
||||
typedef WinGetProcessHeapFn = Pointer Function();
|
||||
final WinGetProcessHeapFn winGetProcessHeap = stdlib
|
||||
.lookupFunction<WinGetProcessHeapFn, WinGetProcessHeapFn>('GetProcessHeap');
|
||||
final Pointer processHeap = winGetProcessHeap();
|
||||
|
||||
typedef WinHeapAllocNative = Pointer Function(Pointer, Uint32, IntPtr);
|
||||
typedef WinHeapAlloc = Pointer Function(Pointer, int, int);
|
||||
final WinHeapAlloc winHeapAlloc =
|
||||
stdlib.lookupFunction<WinHeapAllocNative, WinHeapAlloc>('HeapAlloc');
|
||||
|
||||
typedef WinHeapFreeNative = Int32 Function(
|
||||
Pointer heap, Uint32 flags, Pointer memory);
|
||||
typedef WinHeapFree = int Function(Pointer heap, int flags, Pointer memory);
|
||||
final WinHeapFree winHeapFree =
|
||||
stdlib.lookupFunction<WinHeapFreeNative, WinHeapFree>('HeapFree');
|
||||
|
||||
// ignore: constant_identifier_names
|
||||
const int HEAP_ZERO_MEMORY = 8;
|
||||
|
||||
/// Manages memory on the native heap.
|
||||
///
|
||||
/// Does not initialize newly allocated memory to zero. Use [_CallocAllocator]
|
||||
/// for zero-initialized memory on allocation.
|
||||
///
|
||||
/// For POSIX-based systems, this uses `malloc` and `free`. On Windows, it uses
|
||||
/// `HeapAlloc` and `HeapFree` against the default public heap.
|
||||
class _MallocAllocator implements Allocator {
|
||||
const _MallocAllocator();
|
||||
|
||||
/// Allocates [byteCount] bytes of of unitialized memory on the native heap.
|
||||
///
|
||||
/// For POSIX-based systems, this uses `malloc`. On Windows, it uses
|
||||
/// `HeapAlloc` against the default public heap.
|
||||
///
|
||||
/// Throws an [ArgumentError] if the number of bytes or alignment cannot be
|
||||
/// satisfied.
|
||||
// TODO: Stop ignoring alignment if it's large, for example for SSE data.
|
||||
@override
|
||||
Pointer<T> allocate<T extends NativeType>(int byteCount, {int? alignment}) {
|
||||
Pointer<T> result;
|
||||
if (Platform.isWindows) {
|
||||
result = winHeapAlloc(processHeap, /*flags=*/ 0, byteCount).cast();
|
||||
} else {
|
||||
result = posixMalloc(byteCount).cast();
|
||||
}
|
||||
if (result.address == 0) {
|
||||
throw ArgumentError('Could not allocate $byteCount bytes.');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Releases memory allocated on the native heap.
|
||||
///
|
||||
/// For POSIX-based systems, this uses `free`. On Windows, it uses `HeapFree`
|
||||
/// against the default public heap. It may only be used against pointers
|
||||
/// allocated in a manner equivalent to [allocate].
|
||||
///
|
||||
/// Throws an [ArgumentError] if the memory pointed to by [pointer] cannot be
|
||||
/// freed.
|
||||
///
|
||||
@override
|
||||
void free(Pointer pointer) {
|
||||
if (Platform.isWindows) {
|
||||
if (winHeapFree(processHeap, /*flags=*/ 0, pointer) == 0) {
|
||||
throw ArgumentError('Could not free $pointer.');
|
||||
}
|
||||
} else {
|
||||
posixFree(pointer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages memory on the native heap.
|
||||
///
|
||||
/// Does not initialize newly allocated memory to zero. Use [calloc] for
|
||||
/// zero-initialized memory allocation.
|
||||
///
|
||||
/// For POSIX-based systems, this uses `malloc` and `free`. On Windows, it uses
|
||||
/// `HeapAlloc` and `HeapFree` against the default public heap.
|
||||
const Allocator malloc = _MallocAllocator();
|
||||
|
||||
/// Manages memory on the native heap.
|
||||
///
|
||||
/// Initializes newly allocated memory to zero.
|
||||
///
|
||||
/// For POSIX-based systems, this uses `calloc` and `free`. On Windows, it uses
|
||||
/// `HeapAlloc` with [HEAP_ZERO_MEMORY] and `HeapFree` against the default
|
||||
/// public heap.
|
||||
class _CallocAllocator implements Allocator {
|
||||
const _CallocAllocator();
|
||||
|
||||
/// Allocates [byteCount] bytes of zero-initialized of memory on the native
|
||||
/// heap.
|
||||
///
|
||||
/// For POSIX-based systems, this uses `malloc`. On Windows, it uses
|
||||
/// `HeapAlloc` against the default public heap.
|
||||
///
|
||||
/// Throws an [ArgumentError] if the number of bytes or alignment cannot be
|
||||
/// satisfied.
|
||||
// TODO: Stop ignoring alignment if it's large, for example for SSE data.
|
||||
@override
|
||||
Pointer<T> allocate<T extends NativeType>(int byteCount, {int? alignment}) {
|
||||
Pointer<T> result;
|
||||
if (Platform.isWindows) {
|
||||
result = winHeapAlloc(processHeap, /*flags=*/ HEAP_ZERO_MEMORY, byteCount)
|
||||
.cast();
|
||||
} else {
|
||||
result = posixCalloc(byteCount, 1).cast();
|
||||
}
|
||||
if (result.address == 0) {
|
||||
throw ArgumentError('Could not allocate $byteCount bytes.');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Releases memory allocated on the native heap.
|
||||
///
|
||||
/// For POSIX-based systems, this uses `free`. On Windows, it uses `HeapFree`
|
||||
/// against the default public heap. It may only be used against pointers
|
||||
/// allocated in a manner equivalent to [allocate].
|
||||
///
|
||||
/// Throws an [ArgumentError] if the memory pointed to by [pointer] cannot be
|
||||
/// freed.
|
||||
///
|
||||
@override
|
||||
void free(Pointer pointer) {
|
||||
if (Platform.isWindows) {
|
||||
if (winHeapFree(processHeap, /*flags=*/ 0, pointer) == 0) {
|
||||
throw ArgumentError('Could not free $pointer.');
|
||||
}
|
||||
} else {
|
||||
posixFree(pointer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages memory on the native heap.
|
||||
///
|
||||
/// Initializes newly allocated memory to zero. Use [malloc] for uninitialized
|
||||
/// memory allocation.
|
||||
///
|
||||
/// For POSIX-based systems, this uses `calloc` and `free`. On Windows, it uses
|
||||
/// `HeapAlloc` with [HEAP_ZERO_MEMORY] and `HeapFree` against the default
|
||||
/// public heap.
|
||||
const Allocator calloc = _CallocAllocator();
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
//
|
||||
// Explicit arena used for managing resources.
|
||||
|
||||
import 'dart:ffi';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:media_kit/ffi/src/allocation.dart';
|
||||
|
||||
/// An [Allocator] which frees all allocations at the same time.
|
||||
///
|
||||
/// The arena allows you to allocate heap memory, but ignores calls to [free].
|
||||
/// Instead you call [releaseAll] to release all the allocations at the same
|
||||
/// time.
|
||||
///
|
||||
/// Also allows other resources to be associated with the arena, through the
|
||||
/// [using] method, to have a release function called for them when the arena
|
||||
/// is released.
|
||||
///
|
||||
/// An [Allocator] can be provided to do the actual allocation and freeing.
|
||||
/// Defaults to using [calloc].
|
||||
class Arena implements Allocator {
|
||||
/// The [Allocator] used for allocation and freeing.
|
||||
final Allocator _wrappedAllocator;
|
||||
|
||||
/// Native memory under management by this [Arena].
|
||||
final List<Pointer<NativeType>> _managedMemoryPointers = [];
|
||||
|
||||
/// Callbacks for releasing native resources under management by this [Arena].
|
||||
final List<void Function()> _managedResourceReleaseCallbacks = [];
|
||||
|
||||
bool _inUse = true;
|
||||
|
||||
/// Creates a arena of allocations.
|
||||
///
|
||||
/// The [allocator] is used to do the actual allocation and freeing of
|
||||
/// memory. It defaults to using [calloc].
|
||||
Arena([Allocator allocator = calloc]) : _wrappedAllocator = allocator;
|
||||
|
||||
/// Allocates memory and includes it in the arena.
|
||||
///
|
||||
/// Uses the allocator provided to the [Arena] constructor to do the
|
||||
/// allocation.
|
||||
///
|
||||
/// Throws an [ArgumentError] if the number of bytes or alignment cannot be
|
||||
/// satisfied.
|
||||
@override
|
||||
Pointer<T> allocate<T extends NativeType>(int byteCount, {int? alignment}) {
|
||||
_ensureInUse();
|
||||
final p = _wrappedAllocator.allocate<T>(byteCount, alignment: alignment);
|
||||
_managedMemoryPointers.add(p);
|
||||
return p;
|
||||
}
|
||||
|
||||
/// Registers [resource] in this arena.
|
||||
///
|
||||
/// Executes [releaseCallback] on [releaseAll].
|
||||
///
|
||||
/// Returns [resource] again, to allow for easily inserting
|
||||
/// `arena.using(resource, ...)` where the resource is allocated.
|
||||
T using<T>(T resource, void Function(T) releaseCallback) {
|
||||
_ensureInUse();
|
||||
releaseCallback = Zone.current.bindUnaryCallback(releaseCallback);
|
||||
_managedResourceReleaseCallbacks.add(() => releaseCallback(resource));
|
||||
return resource;
|
||||
}
|
||||
|
||||
/// Registers [releaseResourceCallback] to be executed on [releaseAll].
|
||||
void onReleaseAll(void Function() releaseResourceCallback) {
|
||||
_managedResourceReleaseCallbacks.add(releaseResourceCallback);
|
||||
}
|
||||
|
||||
/// Releases all resources that this [Arena] manages.
|
||||
///
|
||||
/// If [reuse] is `true`, the arena can be used again after resources
|
||||
/// have been released. If not, the default, then the [allocate]
|
||||
/// and [using] methods must not be called after a call to `releaseAll`.
|
||||
///
|
||||
/// If any of the callbacks throw, [releaseAll] is interrupted, and should
|
||||
/// be started again.
|
||||
void releaseAll({bool reuse = false}) {
|
||||
if (!reuse) {
|
||||
_inUse = false;
|
||||
}
|
||||
// The code below is deliberately wirtten to allow allocations to happen
|
||||
// during `releaseAll(reuse:true)`. The arena will still be guaranteed
|
||||
// empty when the `releaseAll` call returns.
|
||||
while (_managedResourceReleaseCallbacks.isNotEmpty) {
|
||||
_managedResourceReleaseCallbacks.removeLast()();
|
||||
}
|
||||
for (final p in _managedMemoryPointers) {
|
||||
_wrappedAllocator.free(p);
|
||||
}
|
||||
_managedMemoryPointers.clear();
|
||||
}
|
||||
|
||||
/// Does nothing, invoke [releaseAll] instead.
|
||||
@override
|
||||
void free(Pointer<NativeType> pointer) {}
|
||||
|
||||
void _ensureInUse() {
|
||||
if (!_inUse) {
|
||||
throw StateError(
|
||||
'Arena no longer in use, `releaseAll(reuse: false)` was called.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs [computation] with a new [Arena], and releases all allocations at the
|
||||
/// end.
|
||||
///
|
||||
/// If the return value of [computation] is a [Future], all allocations are
|
||||
/// released when the future completes.
|
||||
///
|
||||
/// If the isolate is shut down, through `Isolate.kill()`, resources are _not_
|
||||
/// cleaned up.
|
||||
R using<R>(R Function(Arena) computation,
|
||||
[Allocator wrappedAllocator = calloc]) {
|
||||
final arena = Arena(wrappedAllocator);
|
||||
bool isAsync = false;
|
||||
try {
|
||||
final result = computation(arena);
|
||||
if (result is Future) {
|
||||
isAsync = true;
|
||||
return (result.whenComplete(arena.releaseAll) as R);
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
if (!isAsync) {
|
||||
arena.releaseAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a zoned [Arena] to manage native resources.
|
||||
///
|
||||
/// The arena is availabe through [zoneArena].
|
||||
///
|
||||
/// If the isolate is shut down, through `Isolate.kill()`, resources are _not_
|
||||
/// cleaned up.
|
||||
R withZoneArena<R>(R Function() computation,
|
||||
[Allocator wrappedAllocator = calloc]) {
|
||||
final arena = Arena(wrappedAllocator);
|
||||
var arenaHolder = [arena];
|
||||
bool isAsync = false;
|
||||
try {
|
||||
return runZoned(() {
|
||||
final result = computation();
|
||||
if (result is Future) {
|
||||
isAsync = true;
|
||||
return result.whenComplete(() {
|
||||
arena.releaseAll();
|
||||
}) as R;
|
||||
}
|
||||
return result;
|
||||
}, zoneValues: {#_arena: arenaHolder});
|
||||
} finally {
|
||||
if (!isAsync) {
|
||||
arena.releaseAll();
|
||||
arenaHolder.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A zone-specific [Arena].
|
||||
///
|
||||
/// Asynchronous computations can share a [Arena]. Use [withZoneArena] to create
|
||||
/// a new zone with a fresh [Arena], and that arena will then be released
|
||||
/// automatically when the function passed to [withZoneArena] completes.
|
||||
/// All code inside that zone can use `zoneArena` to access the arena.
|
||||
///
|
||||
/// The current arena must not be accessed by code which is not running inside
|
||||
/// a zone created by [withZoneArena].
|
||||
Arena get zoneArena {
|
||||
final arenaHolder = Zone.current[#_arena] as List<Arena>?;
|
||||
if (arenaHolder == null) {
|
||||
throw StateError('Not inside a zone created by `useArena`');
|
||||
}
|
||||
if (arenaHolder.isNotEmpty) {
|
||||
return arenaHolder.single;
|
||||
}
|
||||
throw StateError('Arena has already been cleared with releaseAll.');
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:ffi';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:media_kit/ffi/src/allocation.dart';
|
||||
|
||||
/// The contents of a native zero-terminated array of UTF-16 code units.
|
||||
///
|
||||
/// The Utf16 type itself has no functionality, it's only intended to be used
|
||||
/// through a `Pointer<Utf16>` representing the entire array. This pointer is
|
||||
/// the equivalent of a char pointer (`const wchar_t*`) in C code. The
|
||||
/// individual UTF-16 code units are stored in native byte order.
|
||||
class Utf16 extends Opaque {}
|
||||
|
||||
/// Extension method for converting a`Pointer<Utf16>` to a [String].
|
||||
extension Utf16Pointer on Pointer<Utf16> {
|
||||
/// The number of UTF-16 code units in this zero-terminated UTF-16 string.
|
||||
///
|
||||
/// The UTF-16 code units of the strings are the non-zero code units up to
|
||||
/// the first zero code unit.
|
||||
int get length {
|
||||
_ensureNotNullptr('length');
|
||||
final codeUnits = cast<Uint16>();
|
||||
return _length(codeUnits);
|
||||
}
|
||||
|
||||
/// Converts this UTF-16 encoded string to a Dart string.
|
||||
///
|
||||
/// Decodes the UTF-16 code units of this zero-terminated code unit array as
|
||||
/// Unicode code points and creates a Dart string containing those code
|
||||
/// points.
|
||||
///
|
||||
/// If [length] is provided, zero-termination is ignored and the result can
|
||||
/// contain NUL characters.
|
||||
///
|
||||
/// If [length] is not provided, the returned string is the string up til
|
||||
/// but not including the first NUL character.
|
||||
String toDartString({int? length}) {
|
||||
_ensureNotNullptr('toDartString');
|
||||
final codeUnits = cast<Uint16>();
|
||||
if (length == null) {
|
||||
return _toUnknownLengthString(codeUnits);
|
||||
} else {
|
||||
RangeError.checkNotNegative(length, 'length');
|
||||
return _toKnownLengthString(codeUnits, length);
|
||||
}
|
||||
}
|
||||
|
||||
static String _toKnownLengthString(Pointer<Uint16> codeUnits, int length) =>
|
||||
String.fromCharCodes(codeUnits.asTypedList(length));
|
||||
|
||||
static String _toUnknownLengthString(Pointer<Uint16> codeUnits) {
|
||||
final buffer = StringBuffer();
|
||||
var i = 0;
|
||||
while (true) {
|
||||
final char = codeUnits.elementAt(i).value;
|
||||
if (char == 0) {
|
||||
return buffer.toString();
|
||||
}
|
||||
buffer.writeCharCode(char);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
static int _length(Pointer<Uint16> codeUnits) {
|
||||
var length = 0;
|
||||
while (codeUnits[length] != 0) {
|
||||
length++;
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
void _ensureNotNullptr(String operation) {
|
||||
if (this == nullptr) {
|
||||
throw UnsupportedError(
|
||||
"Operation '$operation' not allowed on a 'nullptr'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension method for converting a [String] to a `Pointer<Utf16>`.
|
||||
extension StringUtf16Pointer on String {
|
||||
/// Creates a zero-terminated [Utf16] code-unit array from this String.
|
||||
///
|
||||
/// If this [String] contains NUL characters, converting it back to a string
|
||||
/// using [Utf16Pointer.toDartString] will truncate the result if a length is
|
||||
/// not passed.
|
||||
///
|
||||
/// Returns an [allocator]-allocated pointer to the result.
|
||||
Pointer<Utf16> toNativeUtf16({Allocator allocator = malloc}) {
|
||||
final units = codeUnits;
|
||||
final Pointer<Uint16> result = allocator<Uint16>(units.length + 1);
|
||||
final Uint16List nativeString = result.asTypedList(units.length + 1);
|
||||
nativeString.setRange(0, units.length, units);
|
||||
nativeString[units.length] = 0;
|
||||
return result.cast();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:ffi';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:media_kit/ffi/src/allocation.dart';
|
||||
|
||||
/// The contents of a native zero-terminated array of UTF-8 code units.
|
||||
///
|
||||
/// The Utf8 type itself has no functionality, it's only intended to be used
|
||||
/// through a `Pointer<Utf8>` representing the entire array. This pointer is
|
||||
/// the equivalent of a char pointer (`const char*`) in C code.
|
||||
class Utf8 extends Opaque {}
|
||||
|
||||
/// Extension method for converting a`Pointer<Utf8>` to a [String].
|
||||
extension Utf8Pointer on Pointer<Utf8> {
|
||||
/// The number of UTF-8 code units in this zero-terminated UTF-8 string.
|
||||
///
|
||||
/// The UTF-8 code units of the strings are the non-zero code units up to the
|
||||
/// first zero code unit.
|
||||
int get length {
|
||||
_ensureNotNullptr('length');
|
||||
final codeUnits = cast<Uint8>();
|
||||
return _length(codeUnits);
|
||||
}
|
||||
|
||||
/// Converts this UTF-8 encoded string to a Dart string.
|
||||
///
|
||||
/// Decodes the UTF-8 code units of this zero-terminated byte array as
|
||||
/// Unicode code points and creates a Dart string containing those code
|
||||
/// points.
|
||||
///
|
||||
/// If [length] is provided, zero-termination is ignored and the result can
|
||||
/// contain NUL characters.
|
||||
///
|
||||
/// If [length] is not provided, the returned string is the string up til
|
||||
/// but not including the first NUL character.
|
||||
String toDartString({int? length}) {
|
||||
_ensureNotNullptr('toDartString');
|
||||
final codeUnits = cast<Uint8>();
|
||||
if (length != null) {
|
||||
RangeError.checkNotNegative(length, 'length');
|
||||
} else {
|
||||
length = _length(codeUnits);
|
||||
}
|
||||
return utf8.decode(codeUnits.asTypedList(length));
|
||||
}
|
||||
|
||||
static int _length(Pointer<Uint8> codeUnits) {
|
||||
var length = 0;
|
||||
while (codeUnits[length] != 0) {
|
||||
length++;
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
void _ensureNotNullptr(String operation) {
|
||||
if (this == nullptr) {
|
||||
throw UnsupportedError(
|
||||
"Operation '$operation' not allowed on a 'nullptr'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension method for converting a [String] to a `Pointer<Utf8>`.
|
||||
extension StringUtf8Pointer on String {
|
||||
/// Creates a zero-terminated [Utf8] code-unit array from this String.
|
||||
///
|
||||
/// If this [String] contains NUL characters, converting it back to a string
|
||||
/// using [Utf8Pointer.toDartString] will truncate the result if a length is
|
||||
/// not passed.
|
||||
///
|
||||
/// Unpaired surrogate code points in this [String] will be encoded as
|
||||
/// replacement characters (U+FFFD, encoded as the bytes 0xEF 0xBF 0xBD) in
|
||||
/// the UTF-8 encoded result. See [Utf8Encoder] for details on encoding.
|
||||
///
|
||||
/// Returns an [allocator]-allocated pointer to the result.
|
||||
Pointer<Utf8> toNativeUtf8({Allocator allocator = malloc}) {
|
||||
final units = utf8.encode(this);
|
||||
final Pointer<Uint8> result = allocator<Uint8>(units.length + 1);
|
||||
final Uint8List nativeString = result.asTypedList(units.length + 1);
|
||||
nativeString.setAll(0, units);
|
||||
nativeString[units.length] = 0;
|
||||
return result.cast();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,29 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: camel_case_types
|
||||
|
||||
export 'package:media_kit/src/media_kit.dart';
|
||||
|
||||
export 'package:media_kit/src/models/audio_device.dart';
|
||||
export 'package:media_kit/src/models/audio_params.dart';
|
||||
export 'package:media_kit/src/models/media/media.dart';
|
||||
export 'package:media_kit/src/models/playable.dart';
|
||||
export 'package:media_kit/src/models/player_log.dart';
|
||||
export 'package:media_kit/src/models/player_state.dart';
|
||||
export 'package:media_kit/src/models/player_stream.dart';
|
||||
export 'package:media_kit/src/models/playlist_mode.dart';
|
||||
export 'package:media_kit/src/models/playlist.dart';
|
||||
export 'package:media_kit/src/models/track.dart';
|
||||
export 'package:media_kit/src/models/video_params.dart';
|
||||
|
||||
export 'package:media_kit/src/legacy.dart';
|
||||
|
||||
export 'package:media_kit/src/player/platform_player.dart';
|
||||
export 'package:media_kit/src/player/player.dart';
|
||||
|
||||
export 'package:media_kit/src/player/native/player/player.dart';
|
||||
export 'package:media_kit/src/player/web/player/player.dart';
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
// ignore_for_file: camel_case_types
|
||||
import 'package:media_kit/src/player/native/player/player.dart';
|
||||
import 'package:media_kit/src/player/web/player/player.dart';
|
||||
|
||||
// ----------------------------------------
|
||||
// BACKWARD COMPATIBILITY
|
||||
// ----------------------------------------
|
||||
|
||||
@Deprecated('Use [NativePlayer] instead')
|
||||
typedef libmpvPlayer = NativePlayer;
|
||||
|
||||
@Deprecated('Use [WebPlayer] instead')
|
||||
typedef webPlayer = WebPlayer;
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import 'package:media_kit/src/player/native/player/player.dart';
|
||||
import 'package:media_kit/src/player/web/player/player.dart';
|
||||
|
||||
/// {@template media_kit}
|
||||
///
|
||||
/// package:media_kit
|
||||
/// -----------------
|
||||
/// A complete video & audio library for Flutter & Dart.
|
||||
///
|
||||
/// {@endtemplate}
|
||||
abstract class MediaKit {
|
||||
static bool _initialized = false;
|
||||
|
||||
/// {@macro media_kit}
|
||||
static void ensureInitialized({String? libmpv}) {
|
||||
if (_initialized) return;
|
||||
|
||||
try {
|
||||
if (UniversalPlatform.isWindows) {
|
||||
nativeEnsureInitialized(libmpv: libmpv);
|
||||
} else if (UniversalPlatform.isLinux) {
|
||||
nativeEnsureInitialized(libmpv: libmpv);
|
||||
} else if (UniversalPlatform.isMacOS) {
|
||||
nativeEnsureInitialized(libmpv: libmpv);
|
||||
} else if (UniversalPlatform.isIOS) {
|
||||
nativeEnsureInitialized(libmpv: libmpv);
|
||||
} else if (UniversalPlatform.isAndroid) {
|
||||
nativeEnsureInitialized(libmpv: libmpv);
|
||||
} else if (UniversalPlatform.isWeb) {
|
||||
webEnsureInitialized(libmpv: libmpv);
|
||||
}
|
||||
_initialized = true;
|
||||
} catch (_) {
|
||||
print(
|
||||
'\n'
|
||||
'${'-' * 80}\n'
|
||||
'media_kit: ERROR: MediaKit.ensureInitialized\n'
|
||||
'This indicates that one or more required dependencies could not be located.\n'
|
||||
'\n'
|
||||
'Refer to "Installation" section of the README for further details:\n'
|
||||
'GitHub : https://github.com/media-kit/media-kit#installation\n'
|
||||
'pub.dev : https://pub.dev/packages/media_kit#installation\n'
|
||||
'\n'
|
||||
'TIP: Copy-paste required packages from the above link to your pubspec.yaml.\n'
|
||||
'\n'
|
||||
'If you recently added the packages, make sure to re-run the project ("hot-restart" & "hot-reload" is not sufficient for native plugins).\n'
|
||||
'${'-' * 80}\n',
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright (c) 2021 & onwards, Domingo Montesdeoca González <DomingoMG97@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
/// {@template audio_device}
|
||||
///
|
||||
/// AudioDevice
|
||||
/// -----------
|
||||
///
|
||||
/// Represents an audio device which may be used for output in [Player].
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class AudioDevice {
|
||||
/// Name.
|
||||
final String name;
|
||||
|
||||
/// Description.
|
||||
final String description;
|
||||
|
||||
/// {@macro audio_device}
|
||||
const AudioDevice(
|
||||
this.name,
|
||||
this.description,
|
||||
);
|
||||
|
||||
/// [AudioDevice] with automatic device selection.
|
||||
factory AudioDevice.auto() => const AudioDevice('auto', '');
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is AudioDevice) {
|
||||
return other.name == name;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => name.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => 'AudioDevice($name, $description)';
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
/// {@template audio_params}
|
||||
///
|
||||
/// AudioParams
|
||||
/// -----------
|
||||
///
|
||||
/// Audio format as output by the audio decoder.
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class AudioParams {
|
||||
/// The sample format as string. This uses the same names as used in other places of mpv.
|
||||
final String? format;
|
||||
|
||||
/// Sample rate.
|
||||
final int? sampleRate;
|
||||
|
||||
/// The channel layout as a string. This is similar to what the --audio-channels accepts.
|
||||
final String? channels;
|
||||
|
||||
/// Number of audio channels.
|
||||
final int? channelCount;
|
||||
|
||||
/// As channels, but instead of the possibly cryptic actual layout sent to the audio device, return a hopefully more human readable form.
|
||||
/// Usually only audio-out-params/hr-channels makes sense.
|
||||
final String? hrChannels;
|
||||
|
||||
/// {@macro audio_params}
|
||||
const AudioParams({
|
||||
this.format,
|
||||
this.sampleRate,
|
||||
this.channels,
|
||||
this.channelCount,
|
||||
this.hrChannels,
|
||||
});
|
||||
|
||||
@override
|
||||
operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is AudioParams &&
|
||||
other.format == format &&
|
||||
other.sampleRate == sampleRate &&
|
||||
other.channels == channels &&
|
||||
other.channelCount == channelCount &&
|
||||
other.hrChannels == hrChannels;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
format.hashCode ^
|
||||
sampleRate.hashCode ^
|
||||
channels.hashCode ^
|
||||
channelCount.hashCode ^
|
||||
hrChannels.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => 'AudioParams('
|
||||
'format: $format, '
|
||||
'sampleRate: $sampleRate, '
|
||||
'channels: $channels, '
|
||||
'channelCount: $channelCount, '
|
||||
'hrChannels: $hrChannels'
|
||||
')';
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
export 'media_native.dart' if (dart.library.html) 'media_web.dart';
|
||||
|
|
@ -1,266 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
import 'dart:io';
|
||||
import 'dart:collection';
|
||||
import 'dart:typed_data';
|
||||
import 'package:uri_parser/uri_parser.dart';
|
||||
import 'package:safe_local_storage/safe_local_storage.dart';
|
||||
|
||||
import 'package:media_kit/src/models/playable.dart';
|
||||
|
||||
import 'package:media_kit/src/player/native/utils/temp_file.dart';
|
||||
import 'package:media_kit/src/player/native/utils/asset_loader.dart';
|
||||
import 'package:media_kit/src/player/native/utils/android_content_uri_provider.dart';
|
||||
|
||||
/// {@template media}
|
||||
///
|
||||
/// Media
|
||||
/// -----
|
||||
///
|
||||
/// A [Media] object to open inside a [Player] for playback.
|
||||
///
|
||||
/// ```dart
|
||||
/// final player = Player();
|
||||
/// final playable = Media('https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4');
|
||||
/// await player.open(playable);
|
||||
/// ```
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class Media extends Playable {
|
||||
/// The [Finalizer] is invoked when the [Media] instance is garbage collected.
|
||||
/// This has been done to:
|
||||
/// 1. Evict the [Media] instance from [cache].
|
||||
/// 2. Close the file descriptor created by [AndroidContentUriProvider] to handle content:// URIs on Android.
|
||||
/// 3. Delete the temporary file created by [Media.memory].
|
||||
static final Finalizer<_MediaFinalizerContext> _finalizer =
|
||||
Finalizer<_MediaFinalizerContext>(
|
||||
(context) async {
|
||||
final uri = context.uri;
|
||||
final memory = context.memory;
|
||||
// Decrement reference count.
|
||||
ref[uri] = ((ref[uri] ?? 0) - 1).clamp(0, 1 << 32);
|
||||
// Remove [Media] instance from [cache] if reference count is 0.
|
||||
if (ref[uri] == 0) {
|
||||
cache.remove(uri);
|
||||
}
|
||||
// content:// : Close the possible file descriptor on Android.
|
||||
try {
|
||||
if (Platform.isAndroid) {
|
||||
final data = Uri.parse(uri);
|
||||
if (data.isScheme('FD')) {
|
||||
final fd = int.parse(data.authority);
|
||||
if (fd > 0) {
|
||||
await AndroidContentUriProvider.closeFileDescriptor(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (exeception, stacktrace) {
|
||||
print(exeception);
|
||||
print(stacktrace);
|
||||
}
|
||||
// Media.memory : Delete the temporary file.
|
||||
try {
|
||||
if (memory) {
|
||||
await File(uri).delete_();
|
||||
}
|
||||
} catch (exeception, stacktrace) {
|
||||
print(exeception);
|
||||
print(stacktrace);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/// URI of the [Media].
|
||||
final String uri;
|
||||
|
||||
/// Additional optional user data.
|
||||
///
|
||||
/// Default: `null`.
|
||||
final Map<String, dynamic>? extras;
|
||||
|
||||
/// HTTP headers.
|
||||
///
|
||||
/// Default: `null`.
|
||||
final Map<String, String>? httpHeaders;
|
||||
|
||||
/// Start position.
|
||||
///
|
||||
/// Default: `null`.
|
||||
final Duration? start;
|
||||
|
||||
/// End position.
|
||||
///
|
||||
/// Default: `null`.
|
||||
final Duration? end;
|
||||
|
||||
/// Whether instance is instantiated from [Media.memory].
|
||||
bool _memory = false;
|
||||
|
||||
/// {@macro media}
|
||||
Media(
|
||||
String resource, {
|
||||
Map<String, dynamic>? extras,
|
||||
Map<String, String>? httpHeaders,
|
||||
this.start,
|
||||
this.end,
|
||||
}) : uri = normalizeURI(resource),
|
||||
extras = extras ?? cache[normalizeURI(resource)]?.extras,
|
||||
httpHeaders =
|
||||
httpHeaders ?? cache[normalizeURI(resource)]?.httpHeaders {
|
||||
// Increment reference count.
|
||||
ref[uri] = ((ref[uri] ?? 0) + 1).clamp(0, 1 << 32);
|
||||
// Store [this] instance in [cache].
|
||||
cache[uri] = _MediaCache(
|
||||
extras: this.extras,
|
||||
httpHeaders: this.httpHeaders,
|
||||
);
|
||||
// Attach [this] instance to [Finalizer].
|
||||
_finalizer.attach(
|
||||
this,
|
||||
_MediaFinalizerContext(
|
||||
uri,
|
||||
_memory,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a [Media] instance from [Uint8List].
|
||||
///
|
||||
/// The [type] parameter is optional and is used to specify the MIME type of the media on web.
|
||||
static Future<Media> memory(
|
||||
Uint8List data, {
|
||||
String? type,
|
||||
}) async {
|
||||
final file = await TempFile.create();
|
||||
await file.write_(data);
|
||||
final instance = Media(file.path);
|
||||
instance._memory = true;
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// Normalizes the passed URI.
|
||||
static String normalizeURI(String uri) {
|
||||
if (uri.startsWith(_kAssetScheme)) {
|
||||
// Handle asset:// scheme. Only for Flutter.
|
||||
return AssetLoader.load(uri);
|
||||
}
|
||||
// content:// URI support for Android.
|
||||
try {
|
||||
if (Platform.isAndroid) {
|
||||
if (Uri.parse(uri).isScheme('CONTENT')) {
|
||||
final fd = AndroidContentUriProvider.openFileDescriptorSync(uri);
|
||||
if (fd > 0) {
|
||||
return 'fd://$fd';
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (exception, stacktrace) {
|
||||
print(exception);
|
||||
print(stacktrace);
|
||||
}
|
||||
// Keep the resulting URI normalization same as used by libmpv internally.
|
||||
// [File] or network URIs.
|
||||
final parser = URIParser(uri);
|
||||
switch (parser.type) {
|
||||
case URIType.file:
|
||||
{
|
||||
return parser.file!.path;
|
||||
}
|
||||
case URIType.network:
|
||||
{
|
||||
return parser.uri!.toString();
|
||||
}
|
||||
default:
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
|
||||
/// For comparing with other [Media] instances.
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is Media) {
|
||||
return other.uri == uri;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// For comparing with other [Media] instances.
|
||||
@override
|
||||
int get hashCode => uri.hashCode;
|
||||
|
||||
/// Creates a copy of [this] instance with the given fields replaced with the new values.
|
||||
Media copyWith({
|
||||
String? uri,
|
||||
Map<String, dynamic>? extras,
|
||||
Map<String, String>? httpHeaders,
|
||||
Duration? start,
|
||||
Duration? end,
|
||||
}) {
|
||||
return Media(
|
||||
uri ?? this.uri,
|
||||
extras: extras ?? this.extras,
|
||||
httpHeaders: httpHeaders ?? this.httpHeaders,
|
||||
start: start ?? this.start,
|
||||
end: end ?? this.end,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'Media($uri, extras: $extras, httpHeaders: $httpHeaders, start: $start, end: $end)';
|
||||
|
||||
/// URI scheme used to identify Flutter assets.
|
||||
static const String _kAssetScheme = 'asset://';
|
||||
|
||||
/// Previously created [Media] instances.
|
||||
/// This [HashMap] is used to retrieve previously set [extras] & [httpHeaders].
|
||||
static final HashMap<String, _MediaCache> cache =
|
||||
HashMap<String, _MediaCache>();
|
||||
|
||||
/// Previously created [Media] instances' reference count.
|
||||
static final HashMap<String, int> ref = HashMap<String, int>();
|
||||
}
|
||||
|
||||
/// {@template _media_cache}
|
||||
/// A simple class to pack optional arguments in [Media] together.
|
||||
/// {@endtemplate}
|
||||
class _MediaCache {
|
||||
/// Additional optional user data.
|
||||
///
|
||||
/// Default: `null`.
|
||||
final Map<String, dynamic>? extras;
|
||||
|
||||
/// HTTP headers.
|
||||
///
|
||||
/// Default: `null`.
|
||||
final Map<String, String>? httpHeaders;
|
||||
|
||||
/// {@macro _media_cache}
|
||||
const _MediaCache({
|
||||
this.extras,
|
||||
this.httpHeaders,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() => '_MediaCache('
|
||||
'extras: $extras, '
|
||||
'httpHeaders: $httpHeaders'
|
||||
')';
|
||||
}
|
||||
|
||||
/// {@template _media_finalizer_context}
|
||||
/// A simple class to pack the required attributes into [Finalizer] argument.
|
||||
/// {@endtemplate}
|
||||
class _MediaFinalizerContext {
|
||||
final String uri;
|
||||
final bool memory;
|
||||
|
||||
const _MediaFinalizerContext(this.uri, this.memory);
|
||||
|
||||
@override
|
||||
String toString() => '_MediaFinalizerContext(uri: $uri, memory: $memory)';
|
||||
}
|
||||
|
|
@ -1,221 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
import 'dart:collection';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:html' as html;
|
||||
|
||||
import 'package:media_kit/src/models/playable.dart';
|
||||
|
||||
import 'package:media_kit/src/player/web/utils/asset_loader.dart';
|
||||
|
||||
/// {@template media}
|
||||
///
|
||||
/// Media
|
||||
/// -----
|
||||
///
|
||||
/// A [Media] object to open inside a [Player] for playback.
|
||||
///
|
||||
/// ```dart
|
||||
/// final player = Player();
|
||||
/// final playable = Media('https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4');
|
||||
/// await player.open(playable);
|
||||
/// ```
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class Media extends Playable {
|
||||
/// The [Finalizer] is invoked when the [Media] instance is garbage collected.
|
||||
/// This has been done to:
|
||||
/// 1. Evict the [Media] instance from [cache].
|
||||
/// 2. Close the file descriptor created by [AndroidContentUriProvider] to handle content:// URIs on Android.
|
||||
/// 3. Revoke the object URL created by [Media.memory] on web.
|
||||
static final Finalizer<_MediaFinalizerContext> _finalizer =
|
||||
Finalizer<_MediaFinalizerContext>((context) async {
|
||||
final uri = context.uri;
|
||||
final memory = context.memory;
|
||||
// Decrement reference count.
|
||||
ref[uri] = ((ref[uri] ?? 0) - 1).clamp(0, 1 << 32);
|
||||
// Remove [Media] instance from [cache] if reference count is 0.
|
||||
if (ref[uri] == 0) {
|
||||
cache.remove(uri);
|
||||
}
|
||||
// Media.memory : Revoke the object URL.
|
||||
try {
|
||||
if (memory) {
|
||||
html.Url.revokeObjectUrl(uri);
|
||||
}
|
||||
} catch (exeception, stacktrace) {
|
||||
print(exeception);
|
||||
print(stacktrace);
|
||||
}
|
||||
});
|
||||
|
||||
/// URI of the [Media].
|
||||
final String uri;
|
||||
|
||||
/// Additional optional user data.
|
||||
///
|
||||
/// Default: `null`.
|
||||
final Map<String, dynamic>? extras;
|
||||
|
||||
/// HTTP headers.
|
||||
///
|
||||
/// Default: `null`.
|
||||
final Map<String, String>? httpHeaders;
|
||||
|
||||
/// Start position.
|
||||
///
|
||||
/// Default: `null`.
|
||||
final Duration? start;
|
||||
|
||||
/// End position.
|
||||
///
|
||||
/// Default: `null`.
|
||||
final Duration? end;
|
||||
|
||||
/// Whether instance is instantiated from [Media.memory].
|
||||
bool _memory = false;
|
||||
|
||||
/// {@macro media}
|
||||
Media(
|
||||
String resource, {
|
||||
Map<String, dynamic>? extras,
|
||||
Map<String, String>? httpHeaders,
|
||||
this.start,
|
||||
this.end,
|
||||
}) : uri = normalizeURI(resource),
|
||||
extras = extras ?? cache[normalizeURI(resource)]?.extras,
|
||||
httpHeaders =
|
||||
httpHeaders ?? cache[normalizeURI(resource)]?.httpHeaders {
|
||||
// Ensure httpHeaders are not null or empty to prevent using unsupported HTTP headers on the web
|
||||
if (httpHeaders != null && httpHeaders.isNotEmpty) {
|
||||
throw UnsupportedError('HTTP headers are not supported on web');
|
||||
}
|
||||
|
||||
// Increment reference count.
|
||||
ref[uri] = ((ref[uri] ?? 0) + 1).clamp(0, 1 << 32);
|
||||
// Store [this] instance in [cache].
|
||||
cache[uri] = _MediaCache(
|
||||
extras: this.extras,
|
||||
httpHeaders: this.httpHeaders,
|
||||
);
|
||||
// Attach [this] instance to [Finalizer].
|
||||
_finalizer.attach(
|
||||
this,
|
||||
_MediaFinalizerContext(
|
||||
uri,
|
||||
_memory,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a [Media] instance from [Uint8List].
|
||||
///
|
||||
/// The [type] parameter is optional and is used to specify the MIME type of the media on web.
|
||||
static Future<Media> memory(
|
||||
Uint8List data, {
|
||||
String? type,
|
||||
}) {
|
||||
final src = html.Url.createObjectUrlFromBlob(html.Blob([data], type));
|
||||
final instance = Media(src);
|
||||
instance._memory = true;
|
||||
return Future.value(instance);
|
||||
}
|
||||
|
||||
/// Normalizes the passed URI.
|
||||
static String normalizeURI(String uri) {
|
||||
if (uri.startsWith(_kAssetScheme)) {
|
||||
// Handle asset:// scheme. Only for Flutter.
|
||||
return AssetLoader.load(uri);
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
/// For comparing with other [Media] instances.
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is Media) {
|
||||
return other.uri == uri;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// For comparing with other [Media] instances.
|
||||
@override
|
||||
int get hashCode => uri.hashCode;
|
||||
|
||||
/// Creates a copy of [this] instance with the given fields replaced with the new values.
|
||||
Media copyWith({
|
||||
String? uri,
|
||||
Map<String, dynamic>? extras,
|
||||
Map<String, String>? httpHeaders,
|
||||
Duration? start,
|
||||
Duration? end,
|
||||
}) {
|
||||
return Media(
|
||||
uri ?? this.uri,
|
||||
extras: extras ?? this.extras,
|
||||
httpHeaders: httpHeaders ?? this.httpHeaders,
|
||||
start: start ?? this.start,
|
||||
end: end ?? this.end,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'Media($uri, extras: $extras, httpHeaders: $httpHeaders, start: $start, end: $end)';
|
||||
|
||||
/// URI scheme used to identify Flutter assets.
|
||||
static const String _kAssetScheme = 'asset://';
|
||||
|
||||
/// Previously created [Media] instances.
|
||||
/// This [HashMap] is used to retrieve previously set [extras] & [httpHeaders].
|
||||
static final HashMap<String, _MediaCache> cache =
|
||||
HashMap<String, _MediaCache>();
|
||||
|
||||
/// Previously created [Media] instances' reference count.
|
||||
static final HashMap<String, int> ref = HashMap<String, int>();
|
||||
}
|
||||
|
||||
/// {@template _media_cache}
|
||||
/// A simple class to pack optional arguments in [Media] together.
|
||||
/// {@endtemplate}
|
||||
class _MediaCache {
|
||||
/// Additional optional user data.
|
||||
///
|
||||
/// Default: `null`.
|
||||
final Map<String, dynamic>? extras;
|
||||
|
||||
/// HTTP headers.
|
||||
///
|
||||
/// Default: `null`.
|
||||
final Map<String, String>? httpHeaders;
|
||||
|
||||
/// {@macro _media_cache}
|
||||
const _MediaCache({
|
||||
this.extras,
|
||||
this.httpHeaders,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() => '_MediaCache('
|
||||
'extras: $extras, '
|
||||
'httpHeaders: $httpHeaders'
|
||||
')';
|
||||
}
|
||||
|
||||
/// {@template _media_finalizer_context}
|
||||
/// A simple class to pack the required attributes into [Finalizer] argument.
|
||||
/// {@endtemplate}
|
||||
class _MediaFinalizerContext {
|
||||
final String uri;
|
||||
final bool memory;
|
||||
|
||||
const _MediaFinalizerContext(this.uri, this.memory);
|
||||
|
||||
@override
|
||||
String toString() => '_MediaFinalizerContext(uri: $uri, memory: $memory)';
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
// A marker interface for accepting both [Media] and [Playlist] in [Player.open].
|
||||
|
||||
/// {@template playable}
|
||||
///
|
||||
/// Playable
|
||||
/// --------
|
||||
///
|
||||
/// A playable item in [Player]. It can be [Media] or [Playlist].
|
||||
///
|
||||
/// {@endtemplate}
|
||||
abstract class Playable {
|
||||
/// {@macro playable}
|
||||
const Playable();
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
/// {@template player_log}
|
||||
///
|
||||
/// PlayerLog
|
||||
/// ---------
|
||||
///
|
||||
/// A log message sent by the libmpv backend.
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class PlayerLog {
|
||||
/// The sender of the message.
|
||||
final String prefix;
|
||||
|
||||
/// The log level.
|
||||
final String level;
|
||||
|
||||
/// The log message.
|
||||
final String text;
|
||||
|
||||
/// {@macro player_log}
|
||||
const PlayerLog({
|
||||
required this.prefix,
|
||||
required this.level,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() => 'PlayerLog(prefix: $prefix, level: $level, text: $text)';
|
||||
}
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:media_kit/src/models/track.dart';
|
||||
import 'package:media_kit/src/models/playlist.dart';
|
||||
import 'package:media_kit/src/models/audio_device.dart';
|
||||
import 'package:media_kit/src/models/audio_params.dart';
|
||||
import 'package:media_kit/src/models/video_params.dart';
|
||||
import 'package:media_kit/src/models/playlist_mode.dart';
|
||||
|
||||
/// {@template player_state}
|
||||
///
|
||||
/// PlayerState
|
||||
/// -----------
|
||||
///
|
||||
/// Instantaneous state of the [Player].
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class PlayerState {
|
||||
/// Currently opened [Media]s.
|
||||
final Playlist playlist;
|
||||
|
||||
/// Whether playing or not.
|
||||
final bool playing;
|
||||
|
||||
/// Whether end of currently playing [Media] has been reached.
|
||||
final bool completed;
|
||||
|
||||
/// Current playback position.
|
||||
final Duration position;
|
||||
|
||||
/// Current playback duration.
|
||||
final Duration duration;
|
||||
|
||||
/// Current volume.
|
||||
final double volume;
|
||||
|
||||
/// Current playback rate.
|
||||
final double rate;
|
||||
|
||||
/// Current pitch.
|
||||
final double pitch;
|
||||
|
||||
/// Whether buffering or not.
|
||||
final bool buffering;
|
||||
|
||||
/// Current buffer position.
|
||||
/// This indicates how much of the stream has been decoded & cached by the demuxer.
|
||||
final Duration buffer;
|
||||
|
||||
/// Current playlist mode.
|
||||
final PlaylistMode playlistMode;
|
||||
|
||||
/// Audio parameters of the currently playing [Media].
|
||||
/// e.g. sample rate, channels, etc.
|
||||
final AudioParams audioParams;
|
||||
|
||||
/// Video parameters of the currently playing [Media].
|
||||
/// e.g. width, height, rotation, etc.
|
||||
final VideoParams videoParams;
|
||||
|
||||
/// Audio bitrate of the currently playing [Media].
|
||||
final double? audioBitrate;
|
||||
|
||||
/// Currently selected [AudioDevice].
|
||||
final AudioDevice audioDevice;
|
||||
|
||||
/// Currently available [AudioDevice]s.
|
||||
final List<AudioDevice> audioDevices;
|
||||
|
||||
/// Currently selected video, audio & subtitle track.
|
||||
final Track track;
|
||||
|
||||
/// Currently available video, audio & subtitle tracks.
|
||||
final Tracks tracks;
|
||||
|
||||
/// Currently playing video's width.
|
||||
final int? width;
|
||||
|
||||
/// Currently playing video's height.
|
||||
final int? height;
|
||||
|
||||
/// Currently displayed subtitle.
|
||||
final List<String> subtitle;
|
||||
|
||||
/// {@macro player_state}
|
||||
const PlayerState({
|
||||
this.playlist = const Playlist([]),
|
||||
this.playing = false,
|
||||
this.completed = false,
|
||||
this.position = Duration.zero,
|
||||
this.duration = Duration.zero,
|
||||
this.volume = 100.0,
|
||||
this.rate = 1.0,
|
||||
this.pitch = 1.0,
|
||||
this.buffering = false,
|
||||
this.buffer = Duration.zero,
|
||||
this.playlistMode = PlaylistMode.none,
|
||||
this.audioParams = const AudioParams(),
|
||||
this.videoParams = const VideoParams(),
|
||||
this.audioBitrate,
|
||||
this.audioDevice = const AudioDevice('auto', ''),
|
||||
this.audioDevices = const [AudioDevice('auto', '')],
|
||||
this.track = const Track(),
|
||||
this.tracks = const Tracks(),
|
||||
this.width,
|
||||
this.height,
|
||||
this.subtitle = const ['', ''],
|
||||
});
|
||||
|
||||
PlayerState copyWith({
|
||||
Playlist? playlist,
|
||||
bool? playing,
|
||||
bool? completed,
|
||||
Duration? position,
|
||||
Duration? duration,
|
||||
double? volume,
|
||||
double? rate,
|
||||
double? pitch,
|
||||
bool? buffering,
|
||||
Duration? buffer,
|
||||
PlaylistMode? playlistMode,
|
||||
AudioParams? audioParams,
|
||||
VideoParams? videoParams,
|
||||
double? audioBitrate,
|
||||
AudioDevice? audioDevice,
|
||||
List<AudioDevice>? audioDevices,
|
||||
Track? track,
|
||||
Tracks? tracks,
|
||||
int? width,
|
||||
int? height,
|
||||
List<String>? subtitle,
|
||||
}) {
|
||||
return PlayerState(
|
||||
playlist: playlist ?? this.playlist,
|
||||
playing: playing ?? this.playing,
|
||||
completed: completed ?? this.completed,
|
||||
position: position ?? this.position,
|
||||
duration: duration ?? this.duration,
|
||||
volume: volume ?? this.volume,
|
||||
rate: rate ?? this.rate,
|
||||
pitch: pitch ?? this.pitch,
|
||||
buffering: buffering ?? this.buffering,
|
||||
buffer: buffer ?? this.buffer,
|
||||
playlistMode: playlistMode ?? this.playlistMode,
|
||||
audioParams: audioParams ?? this.audioParams,
|
||||
videoParams: videoParams ?? this.videoParams,
|
||||
audioBitrate: audioBitrate ?? this.audioBitrate,
|
||||
audioDevice: audioDevice ?? this.audioDevice,
|
||||
audioDevices: audioDevices ?? this.audioDevices,
|
||||
track: track ?? this.track,
|
||||
tracks: tracks ?? this.tracks,
|
||||
width: width ?? this.width,
|
||||
height: height ?? this.height,
|
||||
subtitle: subtitle ?? this.subtitle,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'Player('
|
||||
'playlist: $playlist, '
|
||||
'playing: $playing, '
|
||||
'completed: $completed, '
|
||||
'position: $position, '
|
||||
'duration: $duration, '
|
||||
'volume: $volume, '
|
||||
'rate: $rate, '
|
||||
'pitch: $pitch, '
|
||||
'buffering: $buffering, '
|
||||
'buffer: $buffer, '
|
||||
'playlistMode: $playlistMode, '
|
||||
'audioParams: $audioParams, '
|
||||
'videoParams: $videoParams, '
|
||||
'audioBitrate: $audioBitrate, '
|
||||
'audioDevice: $audioDevice, '
|
||||
'audioDevices: $audioDevices, '
|
||||
'track: $track, '
|
||||
'tracks: $tracks, '
|
||||
'width: $width, '
|
||||
'height: $height, '
|
||||
'subtitle: $subtitle'
|
||||
')';
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:media_kit/src/models/track.dart';
|
||||
import 'package:media_kit/src/models/playlist.dart';
|
||||
import 'package:media_kit/src/models/player_log.dart';
|
||||
import 'package:media_kit/src/models/audio_device.dart';
|
||||
import 'package:media_kit/src/models/audio_params.dart';
|
||||
import 'package:media_kit/src/models/playlist_mode.dart';
|
||||
import 'package:media_kit/src/models/video_params.dart';
|
||||
|
||||
/// {@template player_stream}
|
||||
///
|
||||
/// PlayerStream
|
||||
/// ------------
|
||||
///
|
||||
/// Event [Stream]s for subscribing to [Player] events.
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class PlayerStream {
|
||||
/// Currently opened [Media]s.
|
||||
final Stream<Playlist> playlist;
|
||||
|
||||
/// Whether playing or not.
|
||||
final Stream<bool> playing;
|
||||
|
||||
/// Whether end of currently playing [Media] has been reached.
|
||||
final Stream<bool> completed;
|
||||
|
||||
/// Current playback position.
|
||||
final Stream<Duration> position;
|
||||
|
||||
/// Current playback duration.
|
||||
final Stream<Duration> duration;
|
||||
|
||||
/// Current volume.
|
||||
final Stream<double> volume;
|
||||
|
||||
/// Current playback rate.
|
||||
final Stream<double> rate;
|
||||
|
||||
/// Current pitch.
|
||||
final Stream<double> pitch;
|
||||
|
||||
/// Whether buffering or not.
|
||||
final Stream<bool> buffering;
|
||||
|
||||
/// Current buffer position.
|
||||
/// This indicates how much of the stream has been decoded & cached by the demuxer.
|
||||
final Stream<Duration> buffer;
|
||||
|
||||
/// Current playlist mode.
|
||||
final Stream<PlaylistMode> playlistMode;
|
||||
|
||||
/// Audio parameters of the currently playing [Media].
|
||||
/// e.g. sample rate, channels, etc.
|
||||
final Stream<AudioParams> audioParams;
|
||||
|
||||
/// Video parameters of the currently playing [Media].
|
||||
/// e.g. width, height, rotation etc.
|
||||
final Stream<VideoParams> videoParams;
|
||||
|
||||
/// Audio bitrate of the currently playing [Media].
|
||||
final Stream<double?> audioBitrate;
|
||||
|
||||
/// Currently selected [AudioDevice]s.
|
||||
final Stream<AudioDevice> audioDevice;
|
||||
|
||||
/// Currently available [AudioDevice]s.
|
||||
final Stream<List<AudioDevice>> audioDevices;
|
||||
|
||||
/// Currently selected video, audio and subtitle track.
|
||||
final Stream<Track> track;
|
||||
|
||||
/// Currently available video, audio and subtitle tracks.
|
||||
final Stream<Tracks> tracks;
|
||||
|
||||
/// Currently playing video's width.
|
||||
final Stream<int?> width;
|
||||
|
||||
/// Currently playing video's height.
|
||||
final Stream<int?> height;
|
||||
|
||||
/// Currently displayed subtitle.
|
||||
final Stream<List<String>> subtitle;
|
||||
|
||||
/// [Stream] emitting internal logs.
|
||||
final Stream<PlayerLog> log;
|
||||
|
||||
/// [Stream] emitting error messages. This may be used to handle & display errors to the user.
|
||||
final Stream<String> error;
|
||||
|
||||
/// {@macro player_stream}
|
||||
const PlayerStream(
|
||||
this.playlist,
|
||||
this.playing,
|
||||
this.completed,
|
||||
this.position,
|
||||
this.duration,
|
||||
this.volume,
|
||||
this.rate,
|
||||
this.pitch,
|
||||
this.buffering,
|
||||
this.buffer,
|
||||
this.playlistMode,
|
||||
this.audioParams,
|
||||
this.videoParams,
|
||||
this.audioBitrate,
|
||||
this.audioDevice,
|
||||
this.audioDevices,
|
||||
this.track,
|
||||
this.tracks,
|
||||
this.width,
|
||||
this.height,
|
||||
this.subtitle,
|
||||
this.log,
|
||||
this.error,
|
||||
);
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:media_kit/src/models/playable.dart';
|
||||
import 'package:media_kit/src/models/media/media.dart';
|
||||
|
||||
/// {@template playlist}
|
||||
///
|
||||
/// Playlist
|
||||
/// --------
|
||||
///
|
||||
/// A [Playlist] represents a list of [Media]s & currently playing [index].
|
||||
/// This may be opened in [Player] for playback.
|
||||
///
|
||||
/// ```dart
|
||||
/// final playable = Playlist(
|
||||
/// [
|
||||
/// Media('https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4'),
|
||||
/// Media('https://user-images.githubusercontent.com/28951144/229373709-603a7a89-2105-4e1b-a5a5-a6c3567c9a59.mp4'),
|
||||
/// Media('https://user-images.githubusercontent.com/28951144/229373716-76da0a4e-225a-44e4-9ee7-3e9006dbc3e3.mp4'),
|
||||
/// Media('https://user-images.githubusercontent.com/28951144/229373718-86ce5e1d-d195-45d5-baa6-ef94041d0b90.mp4'),
|
||||
/// Media('https://user-images.githubusercontent.com/28951144/229373720-14d69157-1a56-4a78-a2f4-d7a134d7c3e9.mp4'),
|
||||
/// ],
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class Playlist extends Playable {
|
||||
/// Currently opened [List] of [Media]s.
|
||||
final List<Media> medias;
|
||||
|
||||
/// Currently playing [index].
|
||||
final int index;
|
||||
|
||||
/// {@macro playlist}
|
||||
const Playlist(
|
||||
this.medias, {
|
||||
this.index = 0,
|
||||
});
|
||||
|
||||
Playlist copyWith({
|
||||
List<Media>? medias,
|
||||
int? index,
|
||||
}) {
|
||||
return Playlist(
|
||||
medias ?? this.medias,
|
||||
index: index ?? this.index,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Playlist &&
|
||||
ListEquality().equals(medias, other.medias) &&
|
||||
index == other.index;
|
||||
|
||||
@override
|
||||
int get hashCode => ListEquality().hash(medias) ^ index.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => 'Playlist(medias: $medias, index: $index)';
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
/// {@template playlist_mode}
|
||||
///
|
||||
/// PlaylistMode
|
||||
/// ------------
|
||||
///
|
||||
/// A [PlaylistMode] represents the mode of playback for a [Playlist] loaded in [Player].
|
||||
///
|
||||
/// {@endtemplate}
|
||||
enum PlaylistMode {
|
||||
/// End playback once end of the playlist is reached.
|
||||
none,
|
||||
|
||||
/// Indefinitely loop over the currently playing file in the playlist.
|
||||
single,
|
||||
|
||||
/// Loop over the playlist & restart it from beginning once end is reached.
|
||||
loop,
|
||||
}
|
||||
|
|
@ -1,400 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
/// {@template _track}
|
||||
///
|
||||
/// Track
|
||||
/// -----
|
||||
///
|
||||
/// A video, audio or subtitle track available in [Media].
|
||||
/// This may be selected for output in [Player].
|
||||
///
|
||||
/// {@endtemplate}
|
||||
abstract class _Track {
|
||||
final String id;
|
||||
final String? title;
|
||||
final String? language;
|
||||
// ----------------------------------------
|
||||
final bool? image; /* image */
|
||||
final bool? albumart; /* albumart */
|
||||
final String? codec; /* codec */
|
||||
final String? decoder; /* decoder-desc */
|
||||
final int? w; /* demux-w */
|
||||
final int? h; /* demux-h */
|
||||
final int? channelscount; /* demux-channel-count */
|
||||
final String? channels; /* demux-channels */
|
||||
final int? samplerate; /* demux-samplerate */
|
||||
final double? fps; /* demux-fps */
|
||||
final int? bitrate; /* demux-bitrate */
|
||||
final int? rotate; /* demux-rotate */
|
||||
final double? par; /* demux-par */
|
||||
final int? audiochannels; /* audio-channels */
|
||||
// ----------------------------------------
|
||||
|
||||
/// {@macro _track}
|
||||
const _Track(
|
||||
this.id,
|
||||
this.title,
|
||||
this.language, {
|
||||
this.image,
|
||||
this.albumart,
|
||||
this.codec,
|
||||
this.decoder,
|
||||
this.w,
|
||||
this.h,
|
||||
this.channelscount,
|
||||
this.channels,
|
||||
this.samplerate,
|
||||
this.fps,
|
||||
this.bitrate,
|
||||
this.rotate,
|
||||
this.par,
|
||||
this.audiochannels,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (this is VideoTrack && other is VideoTrack) return id == other.id;
|
||||
if (this is AudioTrack && other is AudioTrack) return id == other.id;
|
||||
if (this is SubtitleTrack && other is SubtitleTrack) return id == other.id;
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
if (this is VideoTrack) return 0x1 ^ id.hashCode;
|
||||
if (this is AudioTrack) return 0x2 ^ id.hashCode;
|
||||
if (this is SubtitleTrack) return 0x3 ^ id.hashCode;
|
||||
return 0x0;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType('
|
||||
'$id, '
|
||||
'$title, '
|
||||
'$language, '
|
||||
'image: $image, '
|
||||
'albumart: $albumart, '
|
||||
'codec: $codec, '
|
||||
'decoder: $decoder, '
|
||||
'w: $w, '
|
||||
'h: $h, '
|
||||
'channelscount: $channelscount, '
|
||||
'channels: $channels, '
|
||||
'samplerate: $samplerate, '
|
||||
'fps: $fps, '
|
||||
'bitrate: $bitrate, '
|
||||
'rotate: $rotate, '
|
||||
'par: $par, '
|
||||
'audiochannels: $audiochannels'
|
||||
')';
|
||||
}
|
||||
|
||||
/// {@template video_track}
|
||||
///
|
||||
/// VideoTrack
|
||||
/// ----------
|
||||
///
|
||||
/// A video available in [Media].
|
||||
/// This may be selected for output in [Player].
|
||||
/// {@endtemplate}
|
||||
class VideoTrack extends _Track {
|
||||
/// {@macro video_track}
|
||||
const VideoTrack(
|
||||
super.id,
|
||||
super.title,
|
||||
super.language, {
|
||||
super.image,
|
||||
super.albumart,
|
||||
super.codec,
|
||||
super.decoder,
|
||||
super.w,
|
||||
super.h,
|
||||
super.channelscount,
|
||||
super.channels,
|
||||
super.samplerate,
|
||||
super.fps,
|
||||
super.bitrate,
|
||||
super.rotate,
|
||||
super.par,
|
||||
super.audiochannels,
|
||||
});
|
||||
|
||||
/// No video track. Disables video output.
|
||||
factory VideoTrack.no() => VideoTrack('no', null, null);
|
||||
|
||||
/// Default video track. Selects the first video track.
|
||||
factory VideoTrack.auto() => VideoTrack('auto', null, null);
|
||||
}
|
||||
|
||||
/// {@template audio_track}
|
||||
///
|
||||
/// AudioTrack
|
||||
/// ----------
|
||||
///
|
||||
/// An audio available in [Media].
|
||||
/// This may be selected for output in [Player].
|
||||
/// {@endtemplate}
|
||||
class AudioTrack extends _Track {
|
||||
/// Whether the audio track is loaded from URI.
|
||||
final bool uri;
|
||||
|
||||
/// {@macro audio_track}
|
||||
const AudioTrack(
|
||||
super.id,
|
||||
super.title,
|
||||
super.language, {
|
||||
super.image,
|
||||
super.albumart,
|
||||
super.codec,
|
||||
super.decoder,
|
||||
super.w,
|
||||
super.h,
|
||||
super.channelscount,
|
||||
super.channels,
|
||||
super.samplerate,
|
||||
super.fps,
|
||||
super.bitrate,
|
||||
super.rotate,
|
||||
super.par,
|
||||
super.audiochannels,
|
||||
this.uri = false,
|
||||
});
|
||||
|
||||
/// No audio track. Disables audio output.
|
||||
factory AudioTrack.no() => AudioTrack('no', null, null);
|
||||
|
||||
/// Default audio track. Selects the first audio track.
|
||||
factory AudioTrack.auto() => AudioTrack('auto', null, null);
|
||||
|
||||
/// [AudioTrack] loaded with URI.
|
||||
///
|
||||
/// This factory constructor may be used to load external audio as URI.
|
||||
///
|
||||
/// **NOTE:** External audio track is automatically unloaded upon playback completion.
|
||||
factory AudioTrack.uri(
|
||||
String uri, {
|
||||
String? title,
|
||||
String? language,
|
||||
}) =>
|
||||
AudioTrack(
|
||||
uri,
|
||||
title,
|
||||
language,
|
||||
uri: true,
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
if (other is AudioTrack) {
|
||||
return id == other.id && uri == other.uri;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => 0x3 ^ id.hashCode ^ uri.hashCode;
|
||||
}
|
||||
|
||||
/// {@template subtitle_track}
|
||||
///
|
||||
/// SubtitleTrack
|
||||
/// -------------
|
||||
///
|
||||
/// A subtitle available in [Media].
|
||||
/// This may be selected for output in [Player].
|
||||
/// {@endtemplate}
|
||||
class SubtitleTrack extends _Track {
|
||||
/// Whether the subtitle track is loaded from URI.
|
||||
final bool uri;
|
||||
|
||||
/// Whether the audio track is loaded from data.
|
||||
final bool data;
|
||||
|
||||
/// {@macro subtitle_track}
|
||||
const SubtitleTrack(
|
||||
super.id,
|
||||
super.title,
|
||||
super.language, {
|
||||
super.image,
|
||||
super.albumart,
|
||||
super.codec,
|
||||
super.decoder,
|
||||
super.w,
|
||||
super.h,
|
||||
super.channelscount,
|
||||
super.channels,
|
||||
super.samplerate,
|
||||
super.fps,
|
||||
super.bitrate,
|
||||
super.rotate,
|
||||
super.par,
|
||||
super.audiochannels,
|
||||
this.uri = false,
|
||||
this.data = false,
|
||||
});
|
||||
|
||||
/// No subtitle track. Disables subtitle output.
|
||||
factory SubtitleTrack.no() => SubtitleTrack('no', null, null);
|
||||
|
||||
/// Default subtitle track. Selects the first subtitle track.
|
||||
factory SubtitleTrack.auto() => SubtitleTrack('auto', null, null);
|
||||
|
||||
/// [SubtitleTrack] loaded with URI.
|
||||
///
|
||||
/// This factory constructor may be used to load external subtitles e.g. SRT, WebVTT etc. as URI.
|
||||
///
|
||||
/// **NOTE:** External audio track is automatically unloaded upon playback completion.
|
||||
factory SubtitleTrack.uri(
|
||||
String uri, {
|
||||
String? title,
|
||||
String? language,
|
||||
}) =>
|
||||
SubtitleTrack(uri, title, language, uri: true);
|
||||
|
||||
/// [SubtitleTrack] loaded with data.
|
||||
///
|
||||
/// This factory constructor may be used to load external subtitles e.g. SRT, WebVTT etc. as data.
|
||||
///
|
||||
/// **NOTE:** External audio track is automatically unloaded upon playback completion.
|
||||
factory SubtitleTrack.data(
|
||||
String data, {
|
||||
String? title,
|
||||
String? language,
|
||||
}) =>
|
||||
SubtitleTrack(data, title, language, data: true);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
if (other is SubtitleTrack) {
|
||||
return id == other.id && uri == other.uri && data == other.data;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => 0x3 ^ id.hashCode ^ uri.hashCode ^ data.hashCode;
|
||||
}
|
||||
|
||||
// For composition in [PlayerState] & [PlayerStreams] classes.
|
||||
|
||||
/// {@template track}
|
||||
///
|
||||
/// Track
|
||||
/// -----
|
||||
///
|
||||
/// Currently selected tracks.
|
||||
/// {@endtemplate}
|
||||
class Track {
|
||||
/// Currently selected video track.
|
||||
final VideoTrack video;
|
||||
|
||||
/// Currently selected audio track.
|
||||
final AudioTrack audio;
|
||||
|
||||
/// Currently selected subtitle track.
|
||||
final SubtitleTrack subtitle;
|
||||
|
||||
/// {@macro track}
|
||||
const Track({
|
||||
this.video = const VideoTrack('auto', null, null),
|
||||
this.audio = const AudioTrack('auto', null, null),
|
||||
this.subtitle = const SubtitleTrack('auto', null, null),
|
||||
});
|
||||
|
||||
Track copyWith({
|
||||
VideoTrack? video,
|
||||
AudioTrack? audio,
|
||||
SubtitleTrack? subtitle,
|
||||
}) {
|
||||
return Track(
|
||||
video: video ?? this.video,
|
||||
audio: audio ?? this.audio,
|
||||
subtitle: subtitle ?? this.subtitle,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is Track) {
|
||||
return video == other.video &&
|
||||
audio == other.audio &&
|
||||
subtitle == other.subtitle;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => video.hashCode ^ audio.hashCode ^ subtitle.hashCode;
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'Track(video: $video, audio: $audio, subtitle: $subtitle)';
|
||||
}
|
||||
|
||||
/// {@template tracks}
|
||||
///
|
||||
/// Tracks
|
||||
/// ------
|
||||
///
|
||||
/// Currently available tracks.
|
||||
/// {@endtemplate}
|
||||
class Tracks {
|
||||
/// Currently available video tracks.
|
||||
final List<VideoTrack> video;
|
||||
|
||||
/// Currently available audio tracks.
|
||||
final List<AudioTrack> audio;
|
||||
|
||||
/// Currently available subtitle tracks.
|
||||
final List<SubtitleTrack> subtitle;
|
||||
|
||||
/// {@macro tracks}
|
||||
const Tracks({
|
||||
this.video = const [
|
||||
VideoTrack('auto', null, null),
|
||||
VideoTrack('no', null, null),
|
||||
],
|
||||
this.audio = const [
|
||||
AudioTrack('auto', null, null),
|
||||
AudioTrack('no', null, null),
|
||||
],
|
||||
this.subtitle = const [
|
||||
SubtitleTrack('auto', null, null),
|
||||
SubtitleTrack('no', null, null),
|
||||
],
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is Tracks) {
|
||||
return ListEquality().equals(video, other.video) &&
|
||||
ListEquality().equals(audio, other.audio) &&
|
||||
ListEquality().equals(subtitle, other.subtitle);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
ListEquality().hash(video) ^
|
||||
ListEquality().hash(audio) ^
|
||||
ListEquality().hash(subtitle);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'Tracks(video: $video, audio: $audio, subtitle: $subtitle)';
|
||||
}
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
/// {@template video_params}
|
||||
///
|
||||
/// VideoParams
|
||||
/// -----------
|
||||
///
|
||||
/// Video parameters, as output by the decoder (with overrides like aspect etc. applied). This has a number of sub-properties.
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class VideoParams {
|
||||
/// The pixel format as string. This uses the same names as used in other places of mpv.
|
||||
final String? pixelformat;
|
||||
|
||||
/// The underlying pixel format as string. This is relevant for some cases of hardware decoding and unavailable otherwise.
|
||||
final String? hwPixelformat;
|
||||
|
||||
/// Video size as integers, with no aspect correction applied.
|
||||
final int? w;
|
||||
|
||||
/// Video size as integers, with no aspect correction applied.
|
||||
final int? h;
|
||||
|
||||
/// Video size as integers, scaled for correct aspect ratio.
|
||||
final int? dw;
|
||||
|
||||
/// Video size as integers, scaled for correct aspect ratio.
|
||||
final int? dh;
|
||||
|
||||
/// Display aspect ratio as float.
|
||||
final double? aspect;
|
||||
|
||||
/// Pixel aspect ratio as float.
|
||||
final double? par;
|
||||
|
||||
/// The colormatrix in use as string.
|
||||
final String? colormatrix;
|
||||
|
||||
/// The colorlevels in use as string.
|
||||
final String? colorlevels;
|
||||
|
||||
/// The colorspace in use as string.
|
||||
final String? primaries;
|
||||
|
||||
/// The gamma function in use as string.
|
||||
final String? gamma;
|
||||
|
||||
/// The video file's tagged signal peak as float.
|
||||
final double? sigPeak;
|
||||
|
||||
/// The light type in use as string.
|
||||
final String? light;
|
||||
|
||||
/// Chroma location as string.
|
||||
final String? chromaLocation;
|
||||
|
||||
/// Intended display rotation in degrees (clockwise).
|
||||
final int? rotate;
|
||||
|
||||
/// Source file stereo 3D mode.
|
||||
final String? stereoIn;
|
||||
|
||||
/// Average bits-per-pixel as integer. Subsampled planar formats use a different resolution, which is the reason this value can sometimes be odd or confusing. Can be unavailable with some formats.
|
||||
final int? averageBpp;
|
||||
|
||||
/// Alpha type. If the format has no alpha channel, this will be unavailable (but in future releases, it could change to no). If alpha is present, this is set to straight or premul.
|
||||
final String? alpha;
|
||||
|
||||
/// {@macro video_params}
|
||||
const VideoParams({
|
||||
this.pixelformat,
|
||||
this.hwPixelformat,
|
||||
this.w,
|
||||
this.h,
|
||||
this.dw,
|
||||
this.dh,
|
||||
this.aspect,
|
||||
this.par,
|
||||
this.colormatrix,
|
||||
this.colorlevels,
|
||||
this.primaries,
|
||||
this.gamma,
|
||||
this.sigPeak,
|
||||
this.light,
|
||||
this.chromaLocation,
|
||||
this.rotate,
|
||||
this.stereoIn,
|
||||
this.averageBpp,
|
||||
this.alpha,
|
||||
});
|
||||
|
||||
@override
|
||||
operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is VideoParams &&
|
||||
other.pixelformat == pixelformat &&
|
||||
other.hwPixelformat == hwPixelformat &&
|
||||
other.w == w &&
|
||||
other.h == h &&
|
||||
other.dw == dw &&
|
||||
other.dh == dh &&
|
||||
other.aspect == aspect &&
|
||||
other.par == par &&
|
||||
other.colormatrix == colormatrix &&
|
||||
other.colorlevels == colorlevels &&
|
||||
other.primaries == primaries &&
|
||||
other.gamma == gamma &&
|
||||
other.sigPeak == sigPeak &&
|
||||
other.light == light &&
|
||||
other.chromaLocation == chromaLocation &&
|
||||
other.rotate == rotate &&
|
||||
other.stereoIn == stereoIn &&
|
||||
other.averageBpp == averageBpp &&
|
||||
other.alpha == alpha;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
pixelformat.hashCode ^
|
||||
hwPixelformat.hashCode ^
|
||||
w.hashCode ^
|
||||
h.hashCode ^
|
||||
dw.hashCode ^
|
||||
dh.hashCode ^
|
||||
aspect.hashCode ^
|
||||
par.hashCode ^
|
||||
colormatrix.hashCode ^
|
||||
colorlevels.hashCode ^
|
||||
primaries.hashCode ^
|
||||
gamma.hashCode ^
|
||||
sigPeak.hashCode ^
|
||||
light.hashCode ^
|
||||
chromaLocation.hashCode ^
|
||||
rotate.hashCode ^
|
||||
stereoIn.hashCode ^
|
||||
averageBpp.hashCode ^
|
||||
alpha.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => 'VideoParams('
|
||||
'pixelformat: $pixelformat, '
|
||||
'hwPixelformat: $hwPixelformat, '
|
||||
'w: $w, '
|
||||
'h: $h, '
|
||||
'dw: $dw, '
|
||||
'dh: $dh, '
|
||||
'aspect: $aspect, '
|
||||
'par: $par, '
|
||||
'colormatrix: $colormatrix, '
|
||||
'colorlevels: $colorlevels, '
|
||||
'primaries: $primaries, '
|
||||
'gamma: $gamma, '
|
||||
'sigPeak: $sigPeak, '
|
||||
'light: $light, '
|
||||
'chromaLocation: $chromaLocation, '
|
||||
'rotate: $rotate, '
|
||||
'stereoIn: $stereoIn, '
|
||||
'averageBpp: $averageBpp, '
|
||||
'alpha: $alpha'
|
||||
')';
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'dart:io';
|
||||
import 'package:uri_parser/uri_parser.dart';
|
||||
import 'package:safe_local_storage/safe_local_storage.dart';
|
||||
|
||||
/// libmpv doesn't seem to read the bitrate from the files which contain bitrate in their stream metadata (not file metadata).
|
||||
/// Typically, I've seen this happening with FLAC & OGG files, since they do not offer the bitrate as a metadata / attached-tags key.
|
||||
///
|
||||
/// Adding this helper class to calculate the bitrate of the FLAC & OGG files manually. Considering FLAC is a lossless format, this approximation should be fine.
|
||||
/// At-least better than the one in libmpv, because it calculates the bitrate from the loaded stream currently in-memory & updates it dynamically as playback progresses.
|
||||
abstract class FallbackBitrateHandler {
|
||||
static bool supported(String uri) => extractFilePath(uri) != null;
|
||||
|
||||
static String? extractFilePath(String uri) {
|
||||
try {
|
||||
// Handle local [File] paths.
|
||||
final parser = URIParser(uri);
|
||||
switch (parser.type) {
|
||||
case URIType.file:
|
||||
{
|
||||
if (['OGG', 'FLAC'].contains(parser.file!.extension)) {
|
||||
return parser.file!.path;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// No support for other URI types.
|
||||
default:
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} catch (exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<double> calculateBitrate(String uri, Duration duration) async {
|
||||
try {
|
||||
final resource = extractFilePath(uri);
|
||||
if (resource != null) {
|
||||
final file = File(resource);
|
||||
final size = await file.length_();
|
||||
final result = size * 8 / duration.inSeconds;
|
||||
return result;
|
||||
}
|
||||
return 0;
|
||||
} catch (exception) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'initializer_isolate.dart';
|
||||
import 'initializer_native_event_loop.dart';
|
||||
|
||||
import 'package:media_kit/generated/libmpv/bindings.dart';
|
||||
|
||||
/// Creates & returns initialized [Pointer<mpv_handle>].
|
||||
/// Pass [path] to libmpv dynamic library & [callback] to receive event callbacks as [Pointer<mpv_event>].
|
||||
///
|
||||
/// Optionally, [options] may be passed to set libmpv options before the initialization.
|
||||
///
|
||||
/// Platform specific threaded event loop is preferred over [Isolate] based event loop (automatic fallback).
|
||||
/// See package:media_kit_native_event_loop for more details.
|
||||
abstract class Initializer {
|
||||
/// Creates & returns initialized [Pointer<mpv_handle>].
|
||||
static Future<Pointer<mpv_handle>> create(
|
||||
String path,
|
||||
Future<void> Function(Pointer<mpv_event> event)? callback, {
|
||||
Map<String, String> options = const {},
|
||||
}) async {
|
||||
try {
|
||||
return await InitializerNativeEventLoop.create(
|
||||
path,
|
||||
callback,
|
||||
options,
|
||||
);
|
||||
} catch (_) {
|
||||
return await InitializerIsolate.create(
|
||||
path,
|
||||
callback,
|
||||
options,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Disposes the event loop of the [Pointer<mpv_handle>] created by [create].
|
||||
/// NOTE: [Pointer<mpv_handle>] itself is not disposed.
|
||||
static void dispose(Pointer<mpv_handle> handle) {
|
||||
try {
|
||||
InitializerNativeEventLoop.dispose(handle);
|
||||
} catch (_) {
|
||||
InitializerIsolate.dispose(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,217 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'dart:ffi';
|
||||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:media_kit/ffi/ffi.dart';
|
||||
import 'package:media_kit/src/values.dart';
|
||||
import 'package:media_kit/generated/libmpv/bindings.dart';
|
||||
|
||||
/// InitializerIsolate
|
||||
/// ------------------
|
||||
///
|
||||
/// Creates & returns initialized [Pointer<mpv_handle>] whose event loop is running on separate [Isolate].
|
||||
abstract class InitializerIsolate {
|
||||
/// Creates & returns initialized [Pointer<mpv_handle>] whose event loop is running on separate [Isolate].
|
||||
static Future<Pointer<mpv_handle>> create(
|
||||
String path,
|
||||
Future<void> Function(Pointer<mpv_event> event)? callback,
|
||||
Map<String, String> options,
|
||||
) async {
|
||||
if (callback == null) {
|
||||
// No requirement for separate isolate.
|
||||
final mpv = MPV(DynamicLibrary.open(path));
|
||||
// Creating [mpv_handle].
|
||||
final handle = mpv.mpv_create();
|
||||
|
||||
// Set custom defined options before [mpv_initialize].
|
||||
for (final entry in options.entries) {
|
||||
final name = entry.key.toNativeUtf8();
|
||||
final value = entry.value.toNativeUtf8();
|
||||
mpv.mpv_set_option_string(
|
||||
handle,
|
||||
name.cast(),
|
||||
value.cast(),
|
||||
);
|
||||
calloc.free(name);
|
||||
calloc.free(value);
|
||||
}
|
||||
|
||||
// Initializing [mpv_handle].
|
||||
mpv.mpv_initialize(handle);
|
||||
return handle;
|
||||
} else {
|
||||
// Used to wait for retrieval of [Pointer<mpv_handle>] from the running [Isolate].
|
||||
final completer = Completer();
|
||||
// Used to receive events from the separate [Isolate].
|
||||
final receiver = ReceivePort();
|
||||
// Late initialized [mpv_handle] & [SendPort] of the [ReceievePort] inside the separate [Isolate].
|
||||
late Pointer<mpv_handle> handle;
|
||||
late SendPort port;
|
||||
// Run [_mainloop] in the separate [Isolate].
|
||||
final isolate = await Isolate.spawn(
|
||||
_mainloop,
|
||||
receiver.sendPort,
|
||||
);
|
||||
receiver.listen(
|
||||
(message) async {
|
||||
// Receiving [SendPort] of the [ReceivePort] inside the separate [Isolate] to:
|
||||
// 1. Send the custom defined options.
|
||||
// 2. Send the path to [DynamicLibrary].
|
||||
if (!completer.isCompleted && message is SendPort) {
|
||||
port = message;
|
||||
port.send(options);
|
||||
port.send(path);
|
||||
}
|
||||
// Receiving [Pointer<mpv_handle>] created by separate [Isolate].
|
||||
else if (!completer.isCompleted && message is int) {
|
||||
handle = Pointer.fromAddress(message);
|
||||
completer.complete();
|
||||
}
|
||||
// Receiving event callbacks.
|
||||
else {
|
||||
Pointer<mpv_event> event = Pointer.fromAddress(message);
|
||||
try {
|
||||
await callback(event);
|
||||
} catch (exception, stacktrace) {
|
||||
print(exception.toString());
|
||||
print(stacktrace.toString());
|
||||
}
|
||||
port.send(true);
|
||||
}
|
||||
},
|
||||
);
|
||||
// Awaiting the retrieval of [Pointer<mpv_handle>].
|
||||
await completer.future;
|
||||
|
||||
// Save the references.
|
||||
_ports[handle.address] = port;
|
||||
_isolates[handle.address] = isolate;
|
||||
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
|
||||
/// Disposes the event loop of the [Pointer<mpv_handle>] created by [create].
|
||||
/// NOTE: [Pointer<mpv_handle>] itself is not disposed.
|
||||
static void dispose(Pointer<mpv_handle> handle) {
|
||||
final port = _ports[handle.address];
|
||||
final isolate = _isolates[handle.address];
|
||||
if (port != null && isolate != null) {
|
||||
port.send(null);
|
||||
_ports.remove(handle.address);
|
||||
_isolates.remove(handle.address);
|
||||
// A voluntary delay. Although, [Isolate.kill] is not necessary since execution in the [Isolate] will stop automatically.
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
isolate.kill(priority: Isolate.immediate);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs on separate [Isolate].
|
||||
/// Calls [MPV.mpv_create] & [MPV.mpv_initialize] to create a new [mpv_handle].
|
||||
/// Uses [MPV.mpv_wait_event] to wait for the next event & notifies through the passed [SendPort] as the argument.
|
||||
///
|
||||
/// First value sent through the [SendPort] is [SendPort] of the internal [ReceivePort].
|
||||
/// Second value sent through the [SendPort] is raw address of the [Pointer<mpv_handle>] created by the [Isolate].
|
||||
/// Subsequent sent values are [Pointer<mpv_event>].
|
||||
static void _mainloop(SendPort port) async {
|
||||
// [Completer] used to ensure that the last [mpv_event] is NOT reset to [mpv_event_id.MPV_EVENT_NONE] after waiting using [MPV.mpv_wait_event] again in the continuously running while loop.
|
||||
var completer = Completer();
|
||||
|
||||
// Used to recieve the confirmation messages from the main thread about successful receive of the sent event through [SendPort].
|
||||
// Upon confirmation, the [Completer] is completed & we jump to next iteration of the while loop waiting with [MPV.mpv_wait_event].
|
||||
final receiver = ReceivePort();
|
||||
|
||||
// Send the [SendPort] of internal [ReceivePort].
|
||||
port.send(receiver.sendPort);
|
||||
|
||||
// Received data from the [SendPort] for initialization.
|
||||
late Map<String, String> options;
|
||||
late MPV mpv;
|
||||
|
||||
bool disposed = false;
|
||||
|
||||
Pointer<mpv_handle>? handle;
|
||||
|
||||
// * First received value is [Map<String, String>] of options.
|
||||
// * Second received value is [String] of path to [DynamicLibrary].
|
||||
// * Subsequent [bool] values are used to notify the successful interpretation of the sent event.
|
||||
// * [null] value is used to dispose the event loop.
|
||||
receiver.listen(
|
||||
(message) {
|
||||
if (message is Map<String, String>) {
|
||||
options = message;
|
||||
} else if (message is String) {
|
||||
mpv = MPV(DynamicLibrary.open(message));
|
||||
completer.complete();
|
||||
} else if (message is bool) {
|
||||
completer.complete();
|
||||
} else if (message == null) {
|
||||
if (handle != null) {
|
||||
// Break out of last event await.
|
||||
completer.complete();
|
||||
// Break out of the possible [MPV.mpv_wait_event] call.
|
||||
mpv.mpv_wakeup(handle);
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Wait for the [Completer] to complete.
|
||||
await completer.future;
|
||||
|
||||
// Creating [mpv_handle].
|
||||
handle ??= mpv.mpv_create();
|
||||
|
||||
// Set custom defined options before [mpv_initialize].
|
||||
for (final entry in options.entries) {
|
||||
final name = entry.key.toNativeUtf8();
|
||||
final value = entry.value.toNativeUtf8();
|
||||
mpv.mpv_set_option_string(
|
||||
handle,
|
||||
name.cast(),
|
||||
value.cast(),
|
||||
);
|
||||
calloc.free(name);
|
||||
calloc.free(value);
|
||||
}
|
||||
|
||||
// Initializing [mpv_handle].
|
||||
mpv.mpv_initialize(handle);
|
||||
|
||||
// Sending the address of the created [mpv_handle] & the [SendPort] of the [receivePort].
|
||||
// Raw address is sent as [int] since we cannot transfer objects through Native Ports, only primatives.
|
||||
port.send(handle.address);
|
||||
|
||||
// Lookup for events & send to main thread through [SendPort].
|
||||
// Ensuring the successful sending of the last event before moving to next [MPV.mpv_wait_event].
|
||||
while (true) {
|
||||
completer = Completer();
|
||||
final event = mpv.mpv_wait_event(handle, kReleaseMode ? -1 : 0.1);
|
||||
|
||||
if (disposed) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (event.ref.event_id != mpv_event_id.MPV_EVENT_NONE) {
|
||||
// Sending raw address of [mpv_event].
|
||||
port.send(event.address);
|
||||
// Ensuring that the last [mpv_event] (which is at the same address) is NOT reset to [mpv_event_id.MPV_EVENT_NONE] after next [MPV.mpv_wait_event] in the loop.
|
||||
await completer.future;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Associated [SendPort] of the [Pointer<mpv_handle>], if events are enabled.
|
||||
static final _ports = HashMap<int, SendPort>();
|
||||
|
||||
/// Associated [Isolate] of the [Pointer<mpv_handle>], if events are enabled.
|
||||
static final _isolates = HashMap<int, Isolate>();
|
||||
}
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'dart:io';
|
||||
import 'dart:ffi';
|
||||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:media_kit/ffi/ffi.dart';
|
||||
|
||||
import 'package:media_kit/generated/libmpv/bindings.dart';
|
||||
|
||||
/// InitializerNativeEventLoop
|
||||
/// --------------------------
|
||||
///
|
||||
/// Creates & returns initialized [Pointer<mpv_handle>] whose event loop is running on native thread.
|
||||
///
|
||||
/// See:
|
||||
/// * https://github.com/media-kit/media-kit/issues/40
|
||||
/// * https://github.com/media-kit/media-kit/pull/46
|
||||
/// * https://github.com/dart-lang/sdk/issues/51254
|
||||
/// * https://github.com/dart-lang/sdk/issues/51261
|
||||
///
|
||||
abstract class InitializerNativeEventLoop {
|
||||
/// Initializes the |InitializerNativeEventLoop| class for usage.
|
||||
static void ensureInitialized() {
|
||||
try {
|
||||
final dylib = () {
|
||||
if (Platform.isMacOS || Platform.isIOS) {
|
||||
return DynamicLibrary.open(
|
||||
'media_kit_native_event_loop.framework/media_kit_native_event_loop',
|
||||
);
|
||||
}
|
||||
if (Platform.isAndroid || Platform.isLinux) {
|
||||
return DynamicLibrary.open('libmedia_kit_native_event_loop.so');
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
return DynamicLibrary.open('media_kit_native_event_loop.dll');
|
||||
}
|
||||
}()!;
|
||||
_register = dylib.lookupFunction<MediaKitEventLoopHandlerRegisterCXX,
|
||||
MediaKitEventLoopHandlerRegisterDart>(
|
||||
'MediaKitEventLoopHandlerRegister',
|
||||
);
|
||||
_notify = dylib.lookupFunction<MediaKitEventLoopHandlerNotifyCXX,
|
||||
MediaKitEventLoopHandlerNotifyDart>(
|
||||
'MediaKitEventLoopHandlerNotify',
|
||||
);
|
||||
_dispose = dylib.lookupFunction<MediaKitEventLoopHandlerDisposeCXX,
|
||||
MediaKitEventLoopHandlerDisposeDart>(
|
||||
'MediaKitEventLoopHandlerDispose',
|
||||
);
|
||||
_initialize = dylib.lookupFunction<MediaKitEventLoopHandlerInitializeCXX,
|
||||
MediaKitEventLoopHandlerInitializeDart>(
|
||||
'MediaKitEventLoopHandlerInitialize',
|
||||
);
|
||||
_initialize?.call();
|
||||
} catch (_) {
|
||||
print(
|
||||
'media_kit: WARNING: package:media_kit_native_event_loop not found.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates & returns initialized [Pointer<mpv_handle>] whose event loop is running on native thread.
|
||||
static Future<Pointer<mpv_handle>> create(
|
||||
String path,
|
||||
Future<void> Function(Pointer<mpv_event> event)? callback,
|
||||
Map<String, String> options,
|
||||
) async {
|
||||
// Native functions from the shared library should be resolved by now. If not, throw an exception.
|
||||
// Primarily, this will happen when the shared library is not found i.e. package:media_kit_native_event_loop is not installed.
|
||||
if (_register == null || _notify == null || _dispose == null) {
|
||||
throw Exception(
|
||||
'package:media_kit_native_event_loop shared library not loaded.',
|
||||
);
|
||||
}
|
||||
|
||||
// Create [mpv_handle] & initialize it.
|
||||
final mpv = MPV(DynamicLibrary.open(path));
|
||||
|
||||
final handle = mpv.mpv_create();
|
||||
|
||||
// Set custom defined options before [mpv_initialize].
|
||||
for (final entry in options.entries) {
|
||||
final name = entry.key.toNativeUtf8();
|
||||
final value = entry.value.toNativeUtf8();
|
||||
mpv.mpv_set_option_string(
|
||||
handle,
|
||||
name.cast(),
|
||||
value.cast(),
|
||||
);
|
||||
calloc.free(name);
|
||||
calloc.free(value);
|
||||
}
|
||||
|
||||
mpv.mpv_initialize(handle);
|
||||
|
||||
// Only register for event callbacks if [callback] is not null.
|
||||
if (callback != null) {
|
||||
// Save [callback] to invoke it inside [ReceivePort] listener.
|
||||
_callbacks[handle.address] = callback;
|
||||
// Register event callback.
|
||||
_register?.call(
|
||||
handle.address,
|
||||
NativeApi.postCObject.cast(),
|
||||
_receiver.sendPort.nativePort,
|
||||
);
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
/// Disposes the event loop of the [Pointer<mpv_handle>] created by [create].
|
||||
/// NOTE: [Pointer<mpv_handle>] itself is not disposed.
|
||||
static void dispose(Pointer<mpv_handle> handle) {
|
||||
// Native functions from the shared library should be resolved by now. If not, throw an exception.
|
||||
// Primarily, this will happen when the shared library is not found i.e. package:media_kit_native_event_loop is not installed.
|
||||
if (_register == null || _notify == null || _dispose == null) {
|
||||
throw Exception(
|
||||
'package:media_kit_native_event_loop shared library not loaded.',
|
||||
);
|
||||
}
|
||||
_dispose?.call(handle.address);
|
||||
_callbacks.remove(handle.address);
|
||||
}
|
||||
|
||||
/// [ReceivePort] used to listen for `mpv_event`(s) from the native event loop.
|
||||
/// A single [ReceivePort] is used for multiple instances.
|
||||
static final _receiver = ReceivePort()
|
||||
..listen(
|
||||
(dynamic message) async {
|
||||
try {
|
||||
final handle = message[0] as int;
|
||||
final event = Pointer<mpv_event>.fromAddress(message[1]);
|
||||
// Notify public event handler.
|
||||
await _callbacks[handle]?.call(event);
|
||||
} catch (exception, stacktrace) {
|
||||
print(exception);
|
||||
print(stacktrace);
|
||||
}
|
||||
// Notify native event loop that event has been handled & it is safe to move onto next `mpv_wait_event`.
|
||||
_notify?.call(message[0]);
|
||||
},
|
||||
);
|
||||
|
||||
// Registered [callback]s to receive [mpv_event](s) from the native event loop.
|
||||
static final _callbacks =
|
||||
HashMap<int, Future<void> Function(Pointer<mpv_event>)>();
|
||||
|
||||
// Resolved native functions from the shared library:
|
||||
|
||||
static MediaKitEventLoopHandlerRegisterDart? _register;
|
||||
static MediaKitEventLoopHandlerNotifyDart? _notify;
|
||||
static MediaKitEventLoopHandlerDisposeDart? _dispose;
|
||||
static MediaKitEventLoopHandlerInitializeDart? _initialize;
|
||||
}
|
||||
|
||||
// Type definitions for native functions in the shared library.
|
||||
|
||||
// C/C++:
|
||||
|
||||
typedef MediaKitEventLoopHandlerRegisterCXX = Void Function(
|
||||
Int64 handle,
|
||||
Pointer<Void> callback,
|
||||
Int64 port,
|
||||
);
|
||||
typedef MediaKitEventLoopHandlerNotifyCXX = Void Function(Int64 handle);
|
||||
typedef MediaKitEventLoopHandlerDisposeCXX = Void Function(Int64 handle);
|
||||
typedef MediaKitEventLoopHandlerInitializeCXX = Void Function();
|
||||
|
||||
// Dart:
|
||||
|
||||
typedef MediaKitEventLoopHandlerRegisterDart = void Function(
|
||||
int handle,
|
||||
Pointer<Void> callback,
|
||||
int port,
|
||||
);
|
||||
typedef MediaKitEventLoopHandlerNotifyDart = void Function(int handle);
|
||||
typedef MediaKitEventLoopHandlerDisposeDart = void Function(int handle);
|
||||
typedef MediaKitEventLoopHandlerInitializeDart = void Function();
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:ffi';
|
||||
|
||||
/// NativeLibrary
|
||||
/// -------------
|
||||
///
|
||||
/// This class is used to discover & load the libmpv shared library.
|
||||
/// It is generally present with the name `libmpv-2.dll` on Windows & `libmpv.so` on GNU/Linux.
|
||||
///
|
||||
abstract class NativeLibrary {
|
||||
/// The resolved libmpv dynamic library.
|
||||
static String get path {
|
||||
if (_resolved == null) {
|
||||
throw Exception(
|
||||
'MediaKit.ensureInitialized must be called before using any API from package:media_kit.',
|
||||
);
|
||||
}
|
||||
return _resolved!;
|
||||
}
|
||||
|
||||
/// Initializes the |NativeLibrary| class for usage.
|
||||
/// This method discovers & loads the libmpv shared library. It is generally present with the name `libmpv-2.dll` on Windows & `libmpv.so` on GNU/Linux.
|
||||
/// The [libmpv] parameter can be used to manually specify the path to the libmpv shared library.
|
||||
static void ensureInitialized({String? libmpv}) {
|
||||
// Attempt to load [libmpv] argument.
|
||||
if (libmpv != null) {
|
||||
DynamicLibrary.open(libmpv);
|
||||
_resolved = libmpv;
|
||||
return;
|
||||
}
|
||||
// Attempt to load [LIBMPV_LIBRARY_PATH] environment variable.
|
||||
try {
|
||||
final env = Platform.environment['LIBMPV_LIBRARY_PATH'];
|
||||
if (env != null) {
|
||||
DynamicLibrary.open(env);
|
||||
_resolved = env;
|
||||
return;
|
||||
}
|
||||
} catch (_) {}
|
||||
// Attempt to load default names.
|
||||
final names = {
|
||||
'windows': [
|
||||
'libmpv-2.dll',
|
||||
'mpv-2.dll',
|
||||
'mpv-1.dll',
|
||||
],
|
||||
'linux': [
|
||||
'libmpv.so',
|
||||
'libmpv.so.2',
|
||||
'libmpv.so.1',
|
||||
],
|
||||
'macos': [
|
||||
'Mpv.framework/Mpv',
|
||||
],
|
||||
'ios': [
|
||||
'Mpv.framework/Mpv',
|
||||
],
|
||||
'android': [
|
||||
'libmpv.so',
|
||||
],
|
||||
}[Platform.operatingSystem];
|
||||
if (names != null) {
|
||||
// Try to load the dynamic library from the system using [DynamicLibrary.open].
|
||||
for (final name in names) {
|
||||
try {
|
||||
DynamicLibrary.open(name);
|
||||
_resolved = name;
|
||||
return;
|
||||
} catch (_) {}
|
||||
}
|
||||
// If the dynamic library is not loaded, throw an [Exception].
|
||||
if (_resolved == null) {
|
||||
throw Exception(
|
||||
{
|
||||
'windows':
|
||||
'Cannot find libmpv-2.dll in your system %PATH%. One way to deal with this is to ship libmpv-2.dll with your compiled executable or script in the same directory.',
|
||||
'linux':
|
||||
'Cannot find libmpv at the usual places. Depending upon your distribution, you can install the libmpv package to make shared library available globally. On Debian or Ubuntu based systems, you can install it with: apt install libmpv-dev.',
|
||||
'macos':
|
||||
'Cannot find Mpv.framework/Mpv. Please ensure it\'s presence in the Frameworks folder of the application.',
|
||||
'ios':
|
||||
'Cannot find Mpv.framework/Mpv. Please ensure it\'s presence in the Frameworks folder of the application.',
|
||||
'android':
|
||||
'Cannot find libmpv.so. Please ensure it\'s presence in the APK.',
|
||||
}[Platform.operatingSystem]!,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw Exception(
|
||||
'Unsupported operating system: ${Platform.operatingSystem}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The resolved libmpv dynamic library.
|
||||
///
|
||||
/// **NOTE:** We are storing this value as [String] because we want to share this across [Isolate]s.
|
||||
static String? _resolved;
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
export 'real.dart' if (dart.library.html) 'stub.dart';
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,18 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'package:media_kit/src/player/platform_player.dart';
|
||||
|
||||
void nativeEnsureInitialized({String? libmpv}) {}
|
||||
|
||||
class NativePlayer extends PlatformPlayer {
|
||||
NativePlayer({required super.configuration});
|
||||
|
||||
/// Whether the [NativePlayer] is initialized for unit-testing.
|
||||
@visibleForTesting
|
||||
static bool test = false;
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'dart:ffi';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:media_kit/ffi/ffi.dart';
|
||||
|
||||
import 'package:media_kit/src/player/native/utils/isolates.dart';
|
||||
|
||||
/// {@template android_asset_loader}
|
||||
///
|
||||
/// AndroidAssetLoader
|
||||
/// ------------------
|
||||
///
|
||||
/// This class is used to access assets bundled with the application on Android.
|
||||
/// The implementation depends on the mediakitandroidhelper library.
|
||||
///
|
||||
/// Learn more: https://github.com/media-kit/media-kit-android-helper
|
||||
///
|
||||
/// {@endtemplate}
|
||||
abstract class AndroidAssetLoader {
|
||||
/// Copies an asset bundled with the application to the external files directory & returns it absolute path.
|
||||
static Future<String> load(String asset) async {
|
||||
final lookup = _loaded[asset];
|
||||
if (lookup != null) {
|
||||
return lookup;
|
||||
}
|
||||
final path = await compute(_copyAssetToFilesDir, asset);
|
||||
_loaded[asset] = path;
|
||||
return path;
|
||||
}
|
||||
|
||||
/// Copies an asset bundled with the application to the files directory & returns it absolute path.
|
||||
static String loadSync(String asset) {
|
||||
final lookup = _loaded[asset];
|
||||
if (lookup != null) {
|
||||
return lookup;
|
||||
}
|
||||
final path = _copyAssetToFilesDir(asset);
|
||||
_loaded[asset] = path;
|
||||
return path;
|
||||
}
|
||||
|
||||
/// The native implementation for [load] & [loadSync].
|
||||
static String _copyAssetToFilesDir(String asset) {
|
||||
final lib = DynamicLibrary.open('libmediakitandroidhelper.so');
|
||||
final fn = lib.lookupFunction<FnCXX, FnDart>(
|
||||
'MediaKitAndroidHelperCopyAssetToFilesDir',
|
||||
);
|
||||
final name = asset.toNativeUtf8();
|
||||
final result = List.generate(4096, (index) => ' ').join('').toNativeUtf8();
|
||||
fn.call(name.cast(), result.cast());
|
||||
final path = result.cast<Utf8>().toDartString().trim();
|
||||
calloc.free(name);
|
||||
calloc.free(result);
|
||||
return path;
|
||||
}
|
||||
|
||||
/// Stores the names of previously loaded assets. This avoids redundant FFI calls.
|
||||
static final HashMap<String, String> _loaded = HashMap<String, String>();
|
||||
}
|
||||
|
||||
// Type definitions for native functions in the shared library.
|
||||
|
||||
// C/C++:
|
||||
|
||||
typedef FnCXX = Void Function(Pointer<Utf8> asset, Pointer<Utf8> result);
|
||||
|
||||
// Dart:
|
||||
|
||||
typedef FnDart = void Function(Pointer<Utf8> asset, Pointer<Utf8> result);
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'dart:ffi';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:media_kit/ffi/ffi.dart';
|
||||
|
||||
import 'package:media_kit/src/player/native/utils/isolates.dart';
|
||||
|
||||
/// {@template android_content_uri_provider}
|
||||
///
|
||||
/// AndroidContentUriProvider
|
||||
/// -------------------------
|
||||
///
|
||||
/// This class is used to access content:// URIs on Android.
|
||||
/// The implementation depends on the mediakitandroidhelper library.
|
||||
///
|
||||
/// Learn more: https://github.com/media-kit/media-kit-android-helper
|
||||
///
|
||||
/// {@endtemplate}
|
||||
abstract class AndroidContentUriProvider {
|
||||
/// Returns the file descriptor of the content:// URI.
|
||||
static Future<int> openFileDescriptor(String uri) async {
|
||||
final lookup = _loaded[uri];
|
||||
if (lookup != null) {
|
||||
return lookup;
|
||||
}
|
||||
final fileDescriptor = await compute(_openFileDescriptor, uri);
|
||||
_loaded[uri] = fileDescriptor;
|
||||
return fileDescriptor;
|
||||
}
|
||||
|
||||
/// Returns the file descriptor of the content:// URI.
|
||||
static int openFileDescriptorSync(String uri) {
|
||||
final lookup = _loaded[uri];
|
||||
if (lookup != null) {
|
||||
return lookup;
|
||||
}
|
||||
final fileDescriptor = _openFileDescriptor(uri);
|
||||
_loaded[uri] = fileDescriptor;
|
||||
return fileDescriptor;
|
||||
}
|
||||
|
||||
/// Closes the file descriptor of the content:// URI.
|
||||
static Future<void> closeFileDescriptor(String uri) async {
|
||||
final lookup = _loaded[uri];
|
||||
if (lookup != null) {
|
||||
_loaded.remove(uri);
|
||||
await compute(_closeFileDescriptor, lookup);
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes the file descriptor of the content:// URI.
|
||||
static void closeFileDescriptorSync(String uri) {
|
||||
final lookup = _loaded[uri];
|
||||
if (lookup != null) {
|
||||
_loaded.remove(uri);
|
||||
_closeFileDescriptor(lookup);
|
||||
}
|
||||
}
|
||||
|
||||
/// The native implementation for [openFileDescriptor] & [openFileDescriptorSync].
|
||||
static int _openFileDescriptor(String uri) {
|
||||
final lib = DynamicLibrary.open('libmediakitandroidhelper.so');
|
||||
final fn =
|
||||
lib.lookupFunction<OpenFileDescriptorCXX, OpenFileDescriptorDart>(
|
||||
'MediaKitAndroidHelperOpenFileDescriptor',
|
||||
);
|
||||
final name = uri.toNativeUtf8();
|
||||
final fileDescriptor = fn.call(name.cast());
|
||||
return fileDescriptor;
|
||||
}
|
||||
|
||||
/// The native implementation for [closeFileDescriptor] & [closeFileDescriptorSync].
|
||||
static void _closeFileDescriptor(int fileDescriptor) {
|
||||
final lib = DynamicLibrary.open('libmediakitandroidhelper.so');
|
||||
final fn =
|
||||
lib.lookupFunction<CloseFileDescriptorCXX, CloseFileDescriptorDart>(
|
||||
'MediaKitAndroidHelperCloseFileDescriptor',
|
||||
);
|
||||
fn.call(fileDescriptor);
|
||||
}
|
||||
|
||||
/// Stores the file descriptors of previously loaded content:// URIs. This avoids redundant FFI calls.
|
||||
static final HashMap<String, int> _loaded = HashMap<String, int>();
|
||||
}
|
||||
|
||||
// Type definitions for native functions in the shared library.
|
||||
|
||||
// C/C++:
|
||||
|
||||
typedef OpenFileDescriptorCXX = Int32 Function(Pointer<Utf8> uri);
|
||||
typedef CloseFileDescriptorCXX = Void Function(Int32 fileDescriptor);
|
||||
|
||||
// Dart:
|
||||
|
||||
typedef OpenFileDescriptorDart = int Function(Pointer<Utf8> uri);
|
||||
typedef CloseFileDescriptorDart = void Function(int fileDescriptor);
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
// ignore_for_file: non_constant_identifier_names, camel_case_types
|
||||
import 'dart:io';
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'package:media_kit/ffi/ffi.dart';
|
||||
|
||||
/// {@template android_helper}
|
||||
///
|
||||
/// AndroidHelper
|
||||
/// -------------
|
||||
///
|
||||
/// Learn more: https://github.com/media-kit/media-kit-android-helper
|
||||
///
|
||||
/// {@endtemplate}
|
||||
abstract class AndroidHelper {
|
||||
/// {@macro android_helper}
|
||||
static void ensureInitialized() {
|
||||
try {
|
||||
if (Platform.isAndroid) {
|
||||
// Load the required shared libraries:
|
||||
// * libmpv.so
|
||||
// * libavcodec.so
|
||||
// * libmediakitandroidhelper.so.
|
||||
DynamicLibrary? libmpv, libavcodec, libmediakitandroidhelper;
|
||||
try {
|
||||
libmpv = DynamicLibrary.open(
|
||||
'libmpv.so',
|
||||
);
|
||||
} catch (_) {}
|
||||
try {
|
||||
libavcodec = DynamicLibrary.open(
|
||||
'libavcodec.so',
|
||||
);
|
||||
} catch (_) {}
|
||||
try {
|
||||
libmediakitandroidhelper = DynamicLibrary.open(
|
||||
'libmediakitandroidhelper.so',
|
||||
);
|
||||
} catch (_) {}
|
||||
// Look for the required symbols.
|
||||
try {
|
||||
_mpv_lavc_set_java_vm = libmpv?.lookupFunction<
|
||||
mpv_lavc_set_java_vmCXX, mpv_lavc_set_java_vmDart>(
|
||||
'mpv_lavc_set_java_vm',
|
||||
);
|
||||
} catch (_) {}
|
||||
try {
|
||||
_av_jni_set_java_vm = libavcodec
|
||||
?.lookupFunction<av_jni_set_java_vmCXX, av_jni_set_java_vmDart>(
|
||||
'av_jni_set_java_vm',
|
||||
);
|
||||
} catch (_) {}
|
||||
try {
|
||||
_MediaKitAndroidHelperGetJavaVM =
|
||||
libmediakitandroidhelper?.lookupFunction<
|
||||
MediaKitAndroidHelperGetJavaVMCXX,
|
||||
MediaKitAndroidHelperGetJavaVMDart>(
|
||||
'MediaKitAndroidHelperGetJavaVM',
|
||||
);
|
||||
} catch (_) {}
|
||||
try {
|
||||
MediaKitAndroidHelperGetFilesDir =
|
||||
libmediakitandroidhelper?.lookupFunction<
|
||||
MediaKitAndroidHelperGetFilesDirCXX,
|
||||
MediaKitAndroidHelperGetFilesDirDart>(
|
||||
'MediaKitAndroidHelperGetFilesDir',
|
||||
);
|
||||
} catch (_) {}
|
||||
try {
|
||||
_MediaKitAndroidHelperIsEmulator =
|
||||
libmediakitandroidhelper?.lookupFunction<
|
||||
MediaKitAndroidHelperIsEmulatorCXX,
|
||||
MediaKitAndroidHelperIsEmulatorDart>(
|
||||
'MediaKitAndroidHelperIsEmulator',
|
||||
);
|
||||
} catch (_) {}
|
||||
try {
|
||||
_MediaKitAndroidHelperGetAPILevel =
|
||||
libmediakitandroidhelper?.lookupFunction<
|
||||
MediaKitAndroidHelperGetAPILevelCXX,
|
||||
MediaKitAndroidHelperGetAPILevelDart>(
|
||||
'MediaKitAndroidHelperGetAPILevel',
|
||||
);
|
||||
} catch (_) {}
|
||||
|
||||
if ((_mpv_lavc_set_java_vm ?? _av_jni_set_java_vm) == null) {
|
||||
throw UnsupportedError(
|
||||
'Cannot load mpv_lavc_set_java_vm (libmpv.so) or av_jni_set_java_vm (libavcodec.so).',
|
||||
);
|
||||
}
|
||||
if (_MediaKitAndroidHelperGetJavaVM == null) {
|
||||
throw UnsupportedError(
|
||||
'Cannot load MediaKitAndroidHelperGetJavaVM (libmediakitandroidhelper.so).',
|
||||
);
|
||||
}
|
||||
|
||||
Pointer<Void>? vm;
|
||||
while (true) {
|
||||
// Invoke av_jni_set_java_vm to set reference to JavaVM*.
|
||||
// It is important to call av_jni_set_java_vm so that libavcodec can access JNI environment & thus, mediacodec APIs.
|
||||
vm = _MediaKitAndroidHelperGetJavaVM?.call();
|
||||
if (vm != null) {
|
||||
if (vm != nullptr) {
|
||||
// FFmpeg may be statically linked with libmpv, in that case libavcodec.so will not be available.
|
||||
// Following patch exposes mpv_lavc_set_java_vm which internally calls av_jni_set_java_vm.
|
||||
// https://github.com/media-kit/libmpv-android-video-build/blob/main/buildscripts/patches/mpv/mpv_lavc_set_java_vm.patch
|
||||
// https://github.com/media-kit/libmpv-android-audio-build/blob/main/buildscripts/patches/mpv/mpv_lavc_set_java_vm.patch
|
||||
final fn = _mpv_lavc_set_java_vm ?? _av_jni_set_java_vm;
|
||||
if (fn != null) {
|
||||
fn(vm);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
sleep(const Duration(milliseconds: 20));
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
static String? get filesDir {
|
||||
if (Platform.isAndroid) {
|
||||
final filesDir = MediaKitAndroidHelperGetFilesDir?.call();
|
||||
if (filesDir != null) {
|
||||
if (filesDir != nullptr) {
|
||||
return filesDir.toDartString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool get isEmulator {
|
||||
if (Platform.isAndroid) {
|
||||
return _MediaKitAndroidHelperIsEmulator?.call() == 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool get isPhysicalDevice {
|
||||
if (Platform.isAndroid) {
|
||||
return !isEmulator;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static int get APILevel {
|
||||
if (Platform.isAndroid) {
|
||||
return _MediaKitAndroidHelperGetAPILevel?.call() ?? -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static av_jni_set_java_vmDart? _av_jni_set_java_vm;
|
||||
static mpv_lavc_set_java_vmDart? _mpv_lavc_set_java_vm;
|
||||
static MediaKitAndroidHelperGetJavaVMDart? _MediaKitAndroidHelperGetJavaVM;
|
||||
static MediaKitAndroidHelperGetFilesDirDart? MediaKitAndroidHelperGetFilesDir;
|
||||
static MediaKitAndroidHelperIsEmulatorDart? _MediaKitAndroidHelperIsEmulator;
|
||||
static MediaKitAndroidHelperGetAPILevelDart?
|
||||
_MediaKitAndroidHelperGetAPILevel;
|
||||
}
|
||||
|
||||
typedef av_jni_set_java_vmCXX = Int32 Function(Pointer<Void> vm);
|
||||
typedef av_jni_set_java_vmDart = int Function(Pointer<Void> vm);
|
||||
|
||||
typedef mpv_lavc_set_java_vmCXX = Int32 Function(Pointer<Void> vm);
|
||||
typedef mpv_lavc_set_java_vmDart = int Function(Pointer<Void> vm);
|
||||
|
||||
typedef MediaKitAndroidHelperGetJavaVMCXX = Pointer<Void> Function();
|
||||
typedef MediaKitAndroidHelperGetJavaVMDart = Pointer<Void> Function();
|
||||
|
||||
typedef MediaKitAndroidHelperGetFilesDirCXX = Pointer<Utf8> Function();
|
||||
typedef MediaKitAndroidHelperGetFilesDirDart = Pointer<Utf8> Function();
|
||||
|
||||
typedef MediaKitAndroidHelperIsEmulatorCXX = Int8 Function();
|
||||
typedef MediaKitAndroidHelperIsEmulatorDart = int Function();
|
||||
|
||||
typedef MediaKitAndroidHelperGetAPILevelCXX = Int32 Function();
|
||||
typedef MediaKitAndroidHelperGetAPILevelDart = int Function();
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'dart:io';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:safe_local_storage/safe_local_storage.dart';
|
||||
|
||||
import 'package:media_kit/src/player/native/utils/android_asset_loader.dart';
|
||||
|
||||
/// {@template asset_loader}
|
||||
///
|
||||
/// AssetLoader
|
||||
/// -----------
|
||||
///
|
||||
/// A utility to load Flutter assets.
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class AssetLoader {
|
||||
static String load(String uri) {
|
||||
final key = encodeAssetKey(uri);
|
||||
final String asset;
|
||||
if (Platform.isWindows) {
|
||||
asset = path.normalize(
|
||||
path.join(
|
||||
path.dirname(Platform.resolvedExecutable),
|
||||
'data',
|
||||
'flutter_assets',
|
||||
key,
|
||||
),
|
||||
);
|
||||
} else if (Platform.isLinux) {
|
||||
asset = path.normalize(
|
||||
path.join(
|
||||
path.dirname(Platform.resolvedExecutable),
|
||||
'data',
|
||||
'flutter_assets',
|
||||
key,
|
||||
),
|
||||
);
|
||||
} else if (Platform.isMacOS) {
|
||||
asset = path.normalize(
|
||||
path.join(
|
||||
path.dirname(Platform.resolvedExecutable),
|
||||
'..',
|
||||
'Frameworks',
|
||||
'App.framework',
|
||||
'Resources',
|
||||
'flutter_assets',
|
||||
key,
|
||||
),
|
||||
);
|
||||
} else if (Platform.isIOS) {
|
||||
asset = path.normalize(
|
||||
path.join(
|
||||
path.dirname(Platform.resolvedExecutable),
|
||||
'Frameworks',
|
||||
'App.framework',
|
||||
'flutter_assets',
|
||||
key,
|
||||
),
|
||||
);
|
||||
} else if (Platform.isAndroid) {
|
||||
asset = path.normalize(
|
||||
AndroidAssetLoader.loadSync(
|
||||
path.join(
|
||||
'flutter_assets',
|
||||
key,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
throw UnimplementedError(
|
||||
'$_kAssetScheme is not supported on ${Platform.operatingSystem}',
|
||||
);
|
||||
}
|
||||
if (!File(asset).existsSync_()) {
|
||||
throw Exception('Unable to load asset: $asset');
|
||||
}
|
||||
return asset;
|
||||
}
|
||||
|
||||
static String encodeAssetKey(String uri) {
|
||||
String key = uri.split(_kAssetScheme).last;
|
||||
if (key.startsWith('/')) {
|
||||
key = key.substring(1);
|
||||
}
|
||||
// https://github.com/media-kit/media-kit/issues/121
|
||||
return key.split('/').map((e) => Uri.encodeComponent(e)).join('/');
|
||||
}
|
||||
|
||||
/// URI scheme used to identify Flutter assets.
|
||||
static const String _kAssetScheme = 'asset://';
|
||||
}
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:isolate';
|
||||
|
||||
/// Signature for the callback passed to [compute].
|
||||
///
|
||||
/// {@macro flutter.foundation.compute.types}
|
||||
///
|
||||
/// Instances of [ComputeCallback] must be functions that can be sent to an
|
||||
/// isolate.
|
||||
/// {@macro flutter.foundation.compute.callback}
|
||||
///
|
||||
/// {@macro flutter.foundation.compute.types}
|
||||
typedef ComputeCallback<Q, R> = FutureOr<R> Function(Q message);
|
||||
|
||||
/// The signature of [compute], which spawns an isolate, runs `callback` on
|
||||
/// that isolate, passes it `message`, and (eventually) returns the value
|
||||
/// returned by `callback`.
|
||||
///
|
||||
/// {@macro flutter.foundation.compute.usecase}
|
||||
///
|
||||
/// The function used as `callback` must be one that can be sent to an isolate.
|
||||
/// {@macro flutter.foundation.compute.callback}
|
||||
///
|
||||
/// {@macro flutter.foundation.compute.types}
|
||||
///
|
||||
/// The `debugLabel` argument can be specified to provide a name to add to the
|
||||
/// [Timeline]. This is useful when profiling an application.
|
||||
typedef ComputeImpl = Future<R> Function<Q, R>(
|
||||
ComputeCallback<Q, R> callback, Q message,
|
||||
{String? debugLabel});
|
||||
|
||||
/// The dart:io implementation of [isolate.compute].
|
||||
Future<R> compute<Q, R>(ComputeCallback<Q, R> callback, Q message,
|
||||
{String? debugLabel}) async {
|
||||
debugLabel ??=
|
||||
bool.fromEnvironment('dart.vm.product') ? 'compute' : callback.toString();
|
||||
|
||||
final Flow flow = Flow.begin();
|
||||
Timeline.startSync('$debugLabel: start', flow: flow);
|
||||
final RawReceivePort port = RawReceivePort();
|
||||
Timeline.finishSync();
|
||||
|
||||
void timeEndAndCleanup() {
|
||||
Timeline.startSync('$debugLabel: end', flow: Flow.end(flow.id));
|
||||
port.close();
|
||||
Timeline.finishSync();
|
||||
}
|
||||
|
||||
final Completer<dynamic> completer = Completer<dynamic>();
|
||||
port.handler = (dynamic msg) {
|
||||
timeEndAndCleanup();
|
||||
completer.complete(msg);
|
||||
};
|
||||
|
||||
try {
|
||||
await Isolate.spawn<_IsolateConfiguration<Q, R>>(
|
||||
_spawn,
|
||||
_IsolateConfiguration<Q, R>(
|
||||
callback,
|
||||
message,
|
||||
port.sendPort,
|
||||
debugLabel,
|
||||
flow.id,
|
||||
),
|
||||
onExit: port.sendPort,
|
||||
onError: port.sendPort,
|
||||
debugName: debugLabel,
|
||||
);
|
||||
} on Object {
|
||||
timeEndAndCleanup();
|
||||
rethrow;
|
||||
}
|
||||
|
||||
final dynamic response = await completer.future;
|
||||
if (response == null) {
|
||||
throw RemoteError('Isolate exited without result or error.', '');
|
||||
}
|
||||
|
||||
assert(response is List<dynamic>);
|
||||
response as List<dynamic>;
|
||||
|
||||
final int type = response.length;
|
||||
assert(1 <= type && type <= 3);
|
||||
|
||||
switch (type) {
|
||||
// success; see _buildSuccessResponse
|
||||
case 1:
|
||||
return response[0] as R;
|
||||
|
||||
// native error; see Isolate.addErrorListener
|
||||
case 2:
|
||||
await Future<Never>.error(RemoteError(
|
||||
response[0] as String,
|
||||
response[1] as String,
|
||||
));
|
||||
|
||||
// caught error; see _buildErrorResponse
|
||||
case 3:
|
||||
default:
|
||||
assert(type == 3 && response[2] == null);
|
||||
|
||||
await Future<Never>.error(
|
||||
response[0] as Object,
|
||||
response[1] as StackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IsolateConfiguration<Q, R> {
|
||||
const _IsolateConfiguration(
|
||||
this.callback,
|
||||
this.message,
|
||||
this.resultPort,
|
||||
this.debugLabel,
|
||||
this.flowId,
|
||||
);
|
||||
final ComputeCallback<Q, R> callback;
|
||||
final Q message;
|
||||
final SendPort resultPort;
|
||||
final String debugLabel;
|
||||
final int flowId;
|
||||
|
||||
FutureOr<R> applyAndTime() {
|
||||
return Timeline.timeSync(
|
||||
debugLabel,
|
||||
() => callback(message),
|
||||
flow: Flow.step(flowId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The spawn point MUST guarantee only one result event is sent through the
|
||||
/// [SendPort.send] be it directly or indirectly i.e. [Isolate.exit].
|
||||
///
|
||||
/// In case an [Error] or [Exception] are thrown AFTER the data
|
||||
/// is sent, they will NOT be handled or reported by the main [Isolate] because
|
||||
/// it stops listening after the first event is received.
|
||||
///
|
||||
/// Also use the helpers [_buildSuccessResponse] and [_buildErrorResponse] to
|
||||
/// build the response
|
||||
Future<void> _spawn<Q, R>(_IsolateConfiguration<Q, R> configuration) async {
|
||||
late final List<dynamic> computationResult;
|
||||
|
||||
try {
|
||||
computationResult =
|
||||
_buildSuccessResponse(await configuration.applyAndTime());
|
||||
} catch (e, s) {
|
||||
computationResult = _buildErrorResponse(e, s);
|
||||
}
|
||||
|
||||
Isolate.exit(configuration.resultPort, computationResult);
|
||||
}
|
||||
|
||||
/// Wrap in [List] to ensure our expectations in the main [Isolate] are met.
|
||||
///
|
||||
/// We need to wrap a success result in a [List] because the user provided type
|
||||
/// [R] could also be a [List]. Meaning, a check `result is R` could return true
|
||||
/// for what was an error event.
|
||||
List<R> _buildSuccessResponse<R>(R result) {
|
||||
return List<R>.filled(1, result);
|
||||
}
|
||||
|
||||
/// Wrap in [List] to ensure our expectations in the main isolate are met.
|
||||
///
|
||||
/// We wrap a caught error in a 3 element [List]. Where the last element is
|
||||
/// always null. We do this so we have a way to know if an error was one we
|
||||
/// caught or one thrown by the library code.
|
||||
List<dynamic> _buildErrorResponse(Object error, StackTrace stack) {
|
||||
return List<dynamic>.filled(3, null)
|
||||
..[0] = error
|
||||
..[1] = stack;
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'dart:io';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'package:media_kit/src/player/native/utils/android_helper.dart';
|
||||
|
||||
/// {@template temp_file}
|
||||
/// TempFile
|
||||
/// --------
|
||||
/// A simple class to create temporary files.
|
||||
/// {@endtemplate}
|
||||
abstract class TempFile {
|
||||
/// Creates a temporary file & returns it.
|
||||
static Future<File> create() async {
|
||||
String? directory;
|
||||
if (Platform.isWindows) {
|
||||
directory = Directory.systemTemp.path;
|
||||
} else if (Platform.isLinux) {
|
||||
directory = Directory.systemTemp.path;
|
||||
} else if (Platform.isMacOS) {
|
||||
directory = Directory.systemTemp.path;
|
||||
} else if (Platform.isIOS) {
|
||||
directory = Directory.systemTemp.path;
|
||||
} else if (Platform.isAndroid) {
|
||||
directory = AndroidHelper.filesDir;
|
||||
}
|
||||
if (directory == null) {
|
||||
throw UnsupportedError('[TempFile.create] is unsupported');
|
||||
}
|
||||
final file = File(join(directory, Uuid().v4()));
|
||||
await file.create();
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,548 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:media_kit/src/models/track.dart';
|
||||
import 'package:media_kit/src/models/playable.dart';
|
||||
import 'package:media_kit/src/models/playlist.dart';
|
||||
import 'package:media_kit/src/models/player_log.dart';
|
||||
import 'package:media_kit/src/models/media/media.dart';
|
||||
import 'package:media_kit/src/models/audio_device.dart';
|
||||
import 'package:media_kit/src/models/audio_params.dart';
|
||||
import 'package:media_kit/src/models/video_params.dart';
|
||||
import 'package:media_kit/src/models/player_state.dart';
|
||||
import 'package:media_kit/src/models/playlist_mode.dart';
|
||||
import 'package:media_kit/src/models/player_stream.dart';
|
||||
|
||||
/// {@template platform_player}
|
||||
/// PlatformPlayer
|
||||
/// --------------
|
||||
///
|
||||
/// This class provides the interface for platform specific [Player] implementations.
|
||||
/// The platform specific implementations are expected to implement the methods accordingly.
|
||||
///
|
||||
/// The subclasses are then used in composition with the [Player] class, based on the platform the application is running on.
|
||||
///
|
||||
/// {@endtemplate}
|
||||
abstract class PlatformPlayer {
|
||||
/// {@macro platform_player}
|
||||
PlatformPlayer({required this.configuration});
|
||||
|
||||
/// User defined configuration for [Player].
|
||||
final PlayerConfiguration configuration;
|
||||
|
||||
/// Current state of the player.
|
||||
late PlayerState state = PlayerState();
|
||||
|
||||
/// Current state of the player available as listenable [Stream]s.
|
||||
late PlayerStream stream = PlayerStream(
|
||||
playlistController.stream.distinct(
|
||||
(previous, current) => previous == current,
|
||||
),
|
||||
playingController.stream.distinct(
|
||||
(previous, current) => previous == current,
|
||||
),
|
||||
completedController.stream.distinct(
|
||||
(previous, current) => previous == current,
|
||||
),
|
||||
positionController.stream.distinct(
|
||||
(previous, current) => previous == current,
|
||||
),
|
||||
durationController.stream.distinct(
|
||||
(previous, current) => previous == current,
|
||||
),
|
||||
volumeController.stream.distinct(
|
||||
(previous, current) => previous == current,
|
||||
),
|
||||
rateController.stream.distinct(
|
||||
(previous, current) => previous == current,
|
||||
),
|
||||
pitchController.stream.distinct(
|
||||
(previous, current) => previous == current,
|
||||
),
|
||||
bufferingController.stream.distinct(
|
||||
(previous, current) => previous == current,
|
||||
),
|
||||
bufferController.stream.distinct(
|
||||
(previous, current) => previous == current,
|
||||
),
|
||||
playlistModeController.stream.distinct(
|
||||
(previous, current) => previous == current,
|
||||
),
|
||||
/* AUDIO-PARAMS STREAM SHOULD NOT BE DISTINCT */
|
||||
audioParamsController.stream,
|
||||
/* VIDEO-PARAMS STREAM SHOULD NOT BE DISTINCT */
|
||||
videoParamsController.stream,
|
||||
audioBitrateController.stream.distinct(
|
||||
(previous, current) => previous == current,
|
||||
),
|
||||
audioDeviceController.stream.distinct(
|
||||
(previous, current) => previous == current,
|
||||
),
|
||||
audioDevicesController.stream.distinct(
|
||||
(previous, current) => ListEquality().equals(previous, current),
|
||||
),
|
||||
trackController.stream.distinct(
|
||||
(previous, current) => previous == current,
|
||||
),
|
||||
tracksController.stream.distinct(
|
||||
(previous, current) => previous == current,
|
||||
),
|
||||
widthController.stream.distinct(
|
||||
(previous, current) => previous == current,
|
||||
),
|
||||
heightController.stream.distinct(
|
||||
(previous, current) => previous == current,
|
||||
),
|
||||
subtitleController.stream.distinct(
|
||||
(previous, current) => ListEquality().equals(previous, current),
|
||||
),
|
||||
logController.stream.distinct(
|
||||
(previous, current) => previous == current,
|
||||
),
|
||||
/* ERROR STREAM SHOULD NOT BE DISTINCT */
|
||||
errorController.stream,
|
||||
);
|
||||
|
||||
@mustCallSuper
|
||||
Future<void> dispose() async {
|
||||
await Future.wait(
|
||||
[
|
||||
playlistController.close(),
|
||||
playingController.close(),
|
||||
completedController.close(),
|
||||
positionController.close(),
|
||||
durationController.close(),
|
||||
volumeController.close(),
|
||||
rateController.close(),
|
||||
pitchController.close(),
|
||||
bufferingController.close(),
|
||||
bufferController.close(),
|
||||
playlistModeController.close(),
|
||||
audioParamsController.close(),
|
||||
videoParamsController.close(),
|
||||
audioBitrateController.close(),
|
||||
audioDeviceController.close(),
|
||||
audioDevicesController.close(),
|
||||
trackController.close(),
|
||||
tracksController.close(),
|
||||
widthController.close(),
|
||||
heightController.close(),
|
||||
subtitleController.close(),
|
||||
logController.close(),
|
||||
errorController.close(),
|
||||
],
|
||||
);
|
||||
for (final callback in release) {
|
||||
try {
|
||||
await callback.call();
|
||||
} catch (exception, stacktrace) {
|
||||
print(exception.toString());
|
||||
print(stacktrace.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> open(
|
||||
Playable playable, {
|
||||
bool play = true,
|
||||
}) {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.open] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> stop() {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.stop] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> play() {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.play] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> pause() {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.pause] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> playOrPause() {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.playOrPause] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> add(Media media) {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.add] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> remove(int index) {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.remove] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> next() {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.next] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> previous() {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.previous] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> jump(int index) {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.jump] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> move(int from, int to) {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.move] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> seek(Duration duration) {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.seek] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setPlaylistMode(PlaylistMode playlistMode) {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.setPlaylistMode] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setVolume(double volume) {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.volume] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setRate(double rate) {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.rate] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setPitch(double pitch) {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.pitch] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setShuffle(bool shuffle) {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.shuffle] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setAudioDevice(AudioDevice audioDevice) {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.setAudioDevice] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setVideoTrack(VideoTrack track) {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.setVideoTrack] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setAudioTrack(AudioTrack track) {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.setAudioTrack] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setSubtitleTrack(SubtitleTrack track) {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.setSubtitleTrack] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<Uint8List?> screenshot({String? format = 'image/jpeg'}) async {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.screenshot] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> get handle {
|
||||
throw UnimplementedError(
|
||||
'[PlatformPlayer.handle] is not implemented',
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
final StreamController<Playlist> playlistController =
|
||||
StreamController<Playlist>.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<bool> playingController =
|
||||
StreamController<bool>.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<bool> completedController =
|
||||
StreamController<bool>.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<Duration> positionController =
|
||||
StreamController<Duration>.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<Duration> durationController =
|
||||
StreamController.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<double> volumeController =
|
||||
StreamController.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<double> rateController =
|
||||
StreamController<double>.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<double> pitchController =
|
||||
StreamController<double>.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<bool> bufferingController =
|
||||
StreamController<bool>.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<Duration> bufferController =
|
||||
StreamController<Duration>.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<PlaylistMode> playlistModeController =
|
||||
StreamController<PlaylistMode>.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<PlayerLog> logController =
|
||||
StreamController<PlayerLog>.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<String> errorController =
|
||||
StreamController<String>.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<AudioParams> audioParamsController =
|
||||
StreamController<AudioParams>.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<VideoParams> videoParamsController =
|
||||
StreamController<VideoParams>.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<double?> audioBitrateController =
|
||||
StreamController<double?>.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<AudioDevice> audioDeviceController =
|
||||
StreamController<AudioDevice>.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<List<AudioDevice>> audioDevicesController =
|
||||
StreamController<List<AudioDevice>>.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<Track> trackController =
|
||||
StreamController<Track>.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<Tracks> tracksController =
|
||||
StreamController<Tracks>.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<int?> widthController =
|
||||
StreamController<int?>.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<int?> heightController =
|
||||
StreamController<int?>.broadcast();
|
||||
|
||||
@protected
|
||||
final StreamController<List<String>> subtitleController =
|
||||
StreamController<List<String>>.broadcast();
|
||||
|
||||
// --------------------------------------------------
|
||||
|
||||
/// [Completer] to wait for initialization of this instance.
|
||||
final Completer<void> completer = Completer<void>();
|
||||
|
||||
/// [Future<void>] to wait for initialization of this instance.
|
||||
Future<void> get waitForPlayerInitialization => completer.future;
|
||||
|
||||
// --------------------------------------------------
|
||||
|
||||
/// [bool] for signaling [VideoController] (from `package:media_kit_video`) initialization.
|
||||
bool isVideoControllerAttached = false;
|
||||
|
||||
/// [Completer] for signaling [VideoController] (from `package:media_kit_video`) initialization.
|
||||
final Completer<void> videoControllerCompleter = Completer<void>();
|
||||
|
||||
/// [Future<void>] to wait for [VideoController] (from `package:media_kit_video`) initialization.
|
||||
Future<void> get waitForVideoControllerInitializationIfAttached {
|
||||
if (isVideoControllerAttached) {
|
||||
return videoControllerCompleter.future;
|
||||
}
|
||||
return Future.value(null);
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
|
||||
/// Publicly defined clean-up [Function]s which must be called before [dispose].
|
||||
final List<Future<void> Function()> release = [];
|
||||
}
|
||||
|
||||
/// {@template player_configuration}
|
||||
///
|
||||
/// PlayerConfiguration
|
||||
/// --------------------
|
||||
/// Configurable options for customizing the [Player] behavior.
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class PlayerConfiguration {
|
||||
/// Sets the video output driver for native backend.
|
||||
///
|
||||
/// Default: `null`.
|
||||
final String? vo;
|
||||
|
||||
/// Enables on-screen controls for native backend.
|
||||
///
|
||||
/// Default: `false`.
|
||||
final bool osc;
|
||||
|
||||
/// Enables or disables pitch shift control for native backend.
|
||||
///
|
||||
/// Enabling this option may result in de-syncing of audio & video.
|
||||
/// Thus, usage in audio only applications is recommended.
|
||||
/// This uses `scaletempo` under the hood & disables `audio-pitch-correction`.
|
||||
///
|
||||
/// See: https://github.com/media-kit/media-kit/issues/45
|
||||
///
|
||||
/// Default: `false`.
|
||||
final bool pitch;
|
||||
|
||||
/// Sets the name of the underlying window & process for native backend.
|
||||
/// This is visible inside the Windows' volume mixer.
|
||||
///
|
||||
/// Default: `null`.
|
||||
final String title;
|
||||
|
||||
/// Optional callback invoked when the internals of the [Player] are initialized & ready for playback.
|
||||
///
|
||||
/// Default: `null`.
|
||||
final void Function()? ready;
|
||||
|
||||
/// Whether [Player] must be started in muted state.
|
||||
///
|
||||
/// Default: `false`.
|
||||
final bool muted;
|
||||
|
||||
/// Whether to use [libass](https://github.com/libass/libass) based subtitle rendering for native backend.
|
||||
///
|
||||
/// By default, subtitles rendering is Flutter `Widget` based.
|
||||
///
|
||||
/// On Android, this option requires [libassAndroidFont] to be set.
|
||||
final bool libass;
|
||||
|
||||
/// Asset name of the `.ttf` font file to be used for [libass](https://github.com/libass/libass) based subtitle rendering on Android.
|
||||
///
|
||||
/// e.g. `assets/fonts/subtitle.ttf`
|
||||
final String? libassAndroidFont;
|
||||
|
||||
/// Sets the log level on native backend.
|
||||
/// Default: `none`.
|
||||
final MPVLogLevel logLevel;
|
||||
|
||||
/// Sets the demuxer cache size (in bytes) for native backend.
|
||||
///
|
||||
/// Default: `32` MB or `32 * 1024 * 1024` bytes.
|
||||
final int bufferSize;
|
||||
|
||||
/// Sets the list of allowed protocols for native backend.
|
||||
///
|
||||
/// Default: `['file', 'tcp', 'tls', 'http', 'https', 'crypto', 'data']`.
|
||||
///
|
||||
/// Learn more: https://ffmpeg.org/ffmpeg-protocols.html#Protocol-Options
|
||||
final List<String> protocolWhitelist;
|
||||
|
||||
/// {@macro player_configuration}
|
||||
const PlayerConfiguration({
|
||||
this.vo = 'null',
|
||||
this.osc = false,
|
||||
this.pitch = false,
|
||||
this.title = 'package:media_kit',
|
||||
this.ready,
|
||||
this.muted = false,
|
||||
this.libass = false,
|
||||
this.libassAndroidFont,
|
||||
this.logLevel = MPVLogLevel.error,
|
||||
this.bufferSize = 32 * 1024 * 1024,
|
||||
this.protocolWhitelist = const [
|
||||
'udp',
|
||||
'rtp',
|
||||
'tcp',
|
||||
'tls',
|
||||
'data',
|
||||
'file',
|
||||
'http',
|
||||
'https',
|
||||
'crypto',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/// {@template mpv_log_level}
|
||||
///
|
||||
/// MPVLogLevel
|
||||
/// --------------------
|
||||
/// Options to customise the [Player] native backend log level.
|
||||
///
|
||||
/// {@endtemplate}
|
||||
enum MPVLogLevel {
|
||||
/// Disable absolutely all messages.
|
||||
/* none, */
|
||||
|
||||
/// Critical/aborting errors.
|
||||
/* fatal, */
|
||||
|
||||
// package:media_kit internally consumes logs of level error.
|
||||
|
||||
/// Simple errors.
|
||||
error,
|
||||
|
||||
/// Possible problems.
|
||||
warn,
|
||||
|
||||
/// Informational message.
|
||||
info,
|
||||
|
||||
/// Noisy informational message.
|
||||
v,
|
||||
|
||||
/// Very noisy technical information.
|
||||
debug,
|
||||
|
||||
/// Extremely noisy.
|
||||
trace,
|
||||
}
|
||||
|
|
@ -1,328 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'dart:typed_data';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import 'package:media_kit/src/models/track.dart';
|
||||
import 'package:media_kit/src/models/playable.dart';
|
||||
import 'package:media_kit/src/models/playlist.dart';
|
||||
import 'package:media_kit/src/models/media/media.dart';
|
||||
import 'package:media_kit/src/models/audio_device.dart';
|
||||
import 'package:media_kit/src/models/player_state.dart';
|
||||
import 'package:media_kit/src/models/playlist_mode.dart';
|
||||
import 'package:media_kit/src/models/player_stream.dart';
|
||||
|
||||
import 'package:media_kit/src/player/native/player/player.dart';
|
||||
import 'package:media_kit/src/player/web/player/player.dart';
|
||||
import 'package:media_kit/src/player/platform_player.dart';
|
||||
|
||||
/// {@template player}
|
||||
///
|
||||
/// Player
|
||||
/// ------
|
||||
///
|
||||
/// [Player] class provides high-level abstraction for media playback.
|
||||
/// Large number of features have been exposed as class methods & properties.
|
||||
///
|
||||
/// The instantaneous state may be accessed using the [state] getter & subscription to the them may be made using the [stream] available.
|
||||
///
|
||||
/// Call [dispose] to free the allocated resources back to the system.
|
||||
///
|
||||
/// ```dart
|
||||
/// import 'package:media_kit/media_kit.dart';
|
||||
///
|
||||
/// MediaKit.ensureInitialized();
|
||||
///
|
||||
/// // Create a [Player] instance for audio or video playback.
|
||||
///
|
||||
/// final player = Player();
|
||||
///
|
||||
/// // Subscribe to event stream & listen to updates.
|
||||
///
|
||||
/// player.stream.playlist.listen((e) => print(e));
|
||||
/// player.stream.playing.listen((e) => print(e));
|
||||
/// player.stream.completed.listen((e) => print(e));
|
||||
/// player.stream.position.listen((e) => print(e));
|
||||
/// player.stream.duration.listen((e) => print(e));
|
||||
/// player.stream.volume.listen((e) => print(e));
|
||||
/// player.stream.rate.listen((e) => print(e));
|
||||
/// player.stream.pitch.listen((e) => print(e));
|
||||
/// player.stream.buffering.listen((e) => print(e));
|
||||
///
|
||||
/// // Open a playable [Media] or [Playlist].
|
||||
///
|
||||
/// await player.open(Media('asset:///assets/videos/sample.mp4'));
|
||||
/// await player.open(Media('file:///C:/Users/Hitesh/Music/Sample.mp3'));
|
||||
/// await player.open(
|
||||
/// Playlist(
|
||||
/// [
|
||||
/// Media('file:///C:/Users/Hitesh/Music/Sample.mp3'),
|
||||
/// Media('file:///C:/Users/Hitesh/Video/Sample.mkv'),
|
||||
/// Media('https://www.example.com/sample.mp4'),
|
||||
/// Media('rtsp://www.example.com/live'),
|
||||
/// ],
|
||||
/// ),
|
||||
/// );
|
||||
///
|
||||
/// // Control playback state.
|
||||
///
|
||||
/// await player.play();
|
||||
/// await player.pause();
|
||||
/// await player.playOrPause();
|
||||
/// await player.seek(const Duration(seconds: 10));
|
||||
///
|
||||
/// // Use or modify the queue.
|
||||
///
|
||||
/// await player.next();
|
||||
/// await player.previous();
|
||||
/// await player.jump(2);
|
||||
/// await player.add(Media('https://www.example.com/sample.mp4'));
|
||||
/// await player.move(0, 2);
|
||||
///
|
||||
/// // Customize speed, pitch, volume, shuffle, playlist mode, audio device.
|
||||
///
|
||||
/// await player.setRate(1.0);
|
||||
/// await player.setPitch(1.2);
|
||||
/// await player.setVolume(50.0);
|
||||
/// await player.setShuffle(false);
|
||||
/// await player.setPlaylistMode(PlaylistMode.loop);
|
||||
/// await player.setAudioDevice(AudioDevice.auto());
|
||||
///
|
||||
/// // Release allocated resources back to the system.
|
||||
///
|
||||
/// await player.dispose();
|
||||
/// ```
|
||||
///
|
||||
/// {@endtemplate}
|
||||
///
|
||||
class Player {
|
||||
/// {@macro player}
|
||||
Player({
|
||||
PlayerConfiguration configuration = const PlayerConfiguration(),
|
||||
}) {
|
||||
if (UniversalPlatform.isWindows) {
|
||||
platform = NativePlayer(configuration: configuration);
|
||||
} else if (UniversalPlatform.isLinux) {
|
||||
platform = NativePlayer(configuration: configuration);
|
||||
} else if (UniversalPlatform.isMacOS) {
|
||||
platform = NativePlayer(configuration: configuration);
|
||||
} else if (UniversalPlatform.isIOS) {
|
||||
platform = NativePlayer(configuration: configuration);
|
||||
} else if (UniversalPlatform.isAndroid) {
|
||||
platform = NativePlayer(configuration: configuration);
|
||||
} else if (UniversalPlatform.isWeb) {
|
||||
platform = WebPlayer(configuration: configuration);
|
||||
}
|
||||
}
|
||||
|
||||
/// Platform specific internal implementation initialized depending upon the current platform.
|
||||
PlatformPlayer? platform;
|
||||
|
||||
/// Current state of the [Player].
|
||||
PlayerState get state => platform!.state;
|
||||
|
||||
/// Current state of the [Player] available as listenable [Stream]s.
|
||||
PlayerStream get stream => platform!.stream;
|
||||
|
||||
/// Current state of the [Player] available as listenable [Stream]s.
|
||||
@Deprecated('Use [stream] instead')
|
||||
PlayerStream get streams => stream;
|
||||
|
||||
/// Disposes the [Player] instance & releases the resources.
|
||||
Future<void> dispose() async {
|
||||
return platform?.dispose();
|
||||
}
|
||||
|
||||
/// Opens a [Media] or [Playlist] into the [Player].
|
||||
/// Passing [play] as `true` starts the playback immediately.
|
||||
///
|
||||
/// ```dart
|
||||
/// await player.open(Media('asset:///assets/videos/sample.mp4'));
|
||||
/// await player.open(Media('file:///C:/Users/Hitesh/Music/Sample.mp3'));
|
||||
/// await player.open(
|
||||
/// Playlist(
|
||||
/// [
|
||||
/// Media('file:///C:/Users/Hitesh/Music/Sample.mp3'),
|
||||
/// Media('file:///C:/Users/Hitesh/Video/Sample.mkv'),
|
||||
/// Media('https://www.example.com/sample.mp4'),
|
||||
/// Media('rtsp://www.example.com/live'),
|
||||
/// ],
|
||||
/// ),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
Future<void> open(
|
||||
Playable playable, {
|
||||
bool play = true,
|
||||
}) async {
|
||||
return platform?.open(
|
||||
playable,
|
||||
play: play,
|
||||
);
|
||||
}
|
||||
|
||||
/// Stops the [Player].
|
||||
/// Unloads the current [Media] or [Playlist] from the [Player]. This method is similar to [dispose] but does not release the resources & [Player] is still usable.
|
||||
Future<void> stop() async {
|
||||
return platform?.stop();
|
||||
}
|
||||
|
||||
/// Starts playing the [Player].
|
||||
Future<void> play() async {
|
||||
return platform?.play();
|
||||
}
|
||||
|
||||
/// Pauses the [Player].
|
||||
Future<void> pause() async {
|
||||
return platform?.pause();
|
||||
}
|
||||
|
||||
/// Cycles between [play] & [pause] states of the [Player].
|
||||
Future<void> playOrPause() async {
|
||||
return platform?.playOrPause();
|
||||
}
|
||||
|
||||
/// Appends a [Media] to the [Player]'s playlist.
|
||||
Future<void> add(Media media) async {
|
||||
return platform?.add(media);
|
||||
}
|
||||
|
||||
/// Removes the [Media] at specified index from the [Player]'s playlist.
|
||||
Future<void> remove(int index) async {
|
||||
return platform?.remove(index);
|
||||
}
|
||||
|
||||
/// Jumps to next [Media] in the [Player]'s playlist.
|
||||
Future<void> next() async {
|
||||
return platform?.next();
|
||||
}
|
||||
|
||||
/// Jumps to previous [Media] in the [Player]'s playlist.
|
||||
Future<void> previous() async {
|
||||
return platform?.previous();
|
||||
}
|
||||
|
||||
/// Jumps to specified [Media]'s index in the [Player]'s playlist.
|
||||
Future<void> jump(int index) async {
|
||||
return platform?.jump(index);
|
||||
}
|
||||
|
||||
/// Moves the playlist [Media] at [from], so that it takes the place of the [Media] [to].
|
||||
Future<void> move(int from, int to) async {
|
||||
return platform?.move(from, to);
|
||||
}
|
||||
|
||||
/// Seeks the currently playing [Media] in the [Player] by specified [Duration].
|
||||
Future<void> seek(Duration duration) async {
|
||||
return platform?.seek(duration);
|
||||
}
|
||||
|
||||
/// Sets playlist mode.
|
||||
Future<void> setPlaylistMode(PlaylistMode playlistMode) async {
|
||||
return platform?.setPlaylistMode(playlistMode);
|
||||
}
|
||||
|
||||
/// Sets the playback volume of the [Player].
|
||||
/// Defaults to `100.0`.
|
||||
Future<void> setVolume(double volume) async {
|
||||
return platform?.setVolume(volume);
|
||||
}
|
||||
|
||||
/// Sets the playback rate of the [Player].
|
||||
/// Defaults to `1.0`.
|
||||
Future<void> setRate(double rate) async {
|
||||
return platform?.setRate(rate);
|
||||
}
|
||||
|
||||
/// Sets the relative pitch of the [Player].
|
||||
/// Defaults to `1.0`.
|
||||
Future<void> setPitch(double pitch) async {
|
||||
return platform?.setPitch(pitch);
|
||||
}
|
||||
|
||||
/// Enables or disables shuffle for [Player].
|
||||
/// Default is `false`.
|
||||
Future<void> setShuffle(bool shuffle) async {
|
||||
return platform?.setShuffle(shuffle);
|
||||
}
|
||||
|
||||
/// Sets the current [AudioDevice] for audio output.
|
||||
///
|
||||
/// * Currently selected [AudioDevice] can be accessed using [state.audioDevice] or [stream.audioDevice].
|
||||
/// * The list of currently available [AudioDevice]s can be obtained accessed using [state.audioDevices] or [stream.audioDevices].
|
||||
Future<void> setAudioDevice(AudioDevice audioDevice) async {
|
||||
return platform?.setAudioDevice(audioDevice);
|
||||
}
|
||||
|
||||
/// Sets the current [VideoTrack] for video output.
|
||||
///
|
||||
/// * Currently selected [VideoTrack] can be accessed using [state.track.video] or [stream.track.video].
|
||||
/// * The list of currently available [VideoTrack]s can be obtained accessed using [state.tracks.video] or [stream.tracks.video].
|
||||
Future<void> setVideoTrack(VideoTrack track) async {
|
||||
return platform?.setVideoTrack(track);
|
||||
}
|
||||
|
||||
/// Sets the current [AudioTrack] for audio output.
|
||||
///
|
||||
/// * Currently selected [AudioTrack] can be accessed using [state.track.audio] or [stream.track.audio].
|
||||
/// * The list of currently available [AudioTrack]s can be obtained accessed using [state.tracks.audio] or [stream.tracks.audio].
|
||||
/// * External audio track can be loaded using [AudioTrack.uri] constructor.
|
||||
///
|
||||
/// ```dart
|
||||
/// player.setAudioTrack(
|
||||
/// AudioTrack.uri(
|
||||
/// 'https://www.iandevlin.com/html5test/webvtt/v/upc-tobymanley.mp4',
|
||||
/// title: 'English',
|
||||
/// language: 'en',
|
||||
/// ),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
Future<void> setAudioTrack(AudioTrack track) async {
|
||||
return platform?.setAudioTrack(track);
|
||||
}
|
||||
|
||||
/// Sets the current [SubtitleTrack] for subtitle output.
|
||||
///
|
||||
/// * Currently selected [SubtitleTrack] can be accessed using [state.track.subtitle] or [stream.track.subtitle].
|
||||
/// * The list of currently available [SubtitleTrack]s can be obtained accessed using [state.tracks.subtitle] or [stream.tracks.subtitle].
|
||||
/// * External subtitle track can be loaded using [SubtitleTrack.uri] or [SubtitleTrack.data] constructor.
|
||||
///
|
||||
/// ```dart
|
||||
/// player.setSubtitleTrack(
|
||||
/// SubtitleTrack.uri(
|
||||
/// 'https://www.iandevlin.com/html5test/webvtt/upc-video-subtitles-en.vtt',
|
||||
/// title: 'English',
|
||||
/// language: 'en',
|
||||
/// ),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
Future<void> setSubtitleTrack(SubtitleTrack track) async {
|
||||
return platform?.setSubtitleTrack(track);
|
||||
}
|
||||
|
||||
/// Takes the snapshot of the current video frame & returns encoded image bytes as [Uint8List].
|
||||
///
|
||||
/// The [format] parameter specifies the format of the image to be returned. Supported values are:
|
||||
/// * `image/jpeg`: Returns a JPEG encoded image.
|
||||
/// * `image/png`: Returns a PNG encoded image.
|
||||
/// * `null`: Returns BGRA pixel buffer.
|
||||
Future<Uint8List?> screenshot({String? format = 'image/jpeg'}) async {
|
||||
return platform?.screenshot(
|
||||
format: format,
|
||||
);
|
||||
}
|
||||
|
||||
/// Internal platform specific identifier for this [Player] instance.
|
||||
///
|
||||
/// Since, [int] is a primitive type, it can be used to pass this [Player] instance to native code without directly depending upon this library.
|
||||
///
|
||||
Future<int> get handle {
|
||||
final result = platform?.handle;
|
||||
return result!;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
export 'stub.dart' if (dart.library.html) 'real.dart';
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,18 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'package:media_kit/src/player/platform_player.dart';
|
||||
|
||||
void webEnsureInitialized({String? libmpv}) {}
|
||||
|
||||
class WebPlayer extends PlatformPlayer {
|
||||
WebPlayer({required super.configuration});
|
||||
|
||||
/// Whether the [WebPlayer] is initialized for unit-testing.
|
||||
@visibleForTesting
|
||||
static bool test = false;
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'package:media_kit/src/values.dart';
|
||||
|
||||
/// {@template asset_loader}
|
||||
///
|
||||
/// AssetLoader
|
||||
/// -----------
|
||||
///
|
||||
/// A utility to load Flutter assets.
|
||||
///
|
||||
/// {@endtemplate}
|
||||
class AssetLoader {
|
||||
static String load(String uri) {
|
||||
return encodeAssetKey(uri);
|
||||
}
|
||||
|
||||
static String encodeAssetKey(String uri) {
|
||||
String key = uri.split(_kAssetScheme).last;
|
||||
if (key.startsWith('/')) {
|
||||
key = key.substring(1);
|
||||
}
|
||||
|
||||
// https://github.com/media-kit/media-kit/issues/531
|
||||
// https://github.com/media-kit/media-kit/issues/121
|
||||
if (kReleaseMode) {
|
||||
return 'assets/${key.split('/').map((e) => Uri.encodeComponent(Uri.encodeComponent(e))).join('/')}';
|
||||
}
|
||||
return key.split('/').map(Uri.encodeComponent).join('/');
|
||||
}
|
||||
|
||||
/// URI scheme used to identify Flutter assets.
|
||||
static const String _kAssetScheme = 'asset://';
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
/// The "length" of a video which doesn't have finite duration.
|
||||
// See: https://github.com/flutter/flutter/issues/107882
|
||||
const Duration jsCompatibleTimeUnset = Duration(
|
||||
milliseconds: -9007199254740990, // Number.MIN_SAFE_INTEGER + 1. -(2^53 - 1)
|
||||
);
|
||||
|
||||
/// Converts a `num` duration coming from a [VideoElement] into a [Duration] that
|
||||
/// the plugin can use.
|
||||
///
|
||||
/// From the documentation, `videoDuration` is "a double-precision floating-point
|
||||
/// value indicating the duration of the media in seconds.
|
||||
/// If no media data is available, the value `NaN` is returned.
|
||||
/// If the element's media doesn't have a known duration —such as for live media
|
||||
/// streams— the value of duration is `+Infinity`."
|
||||
///
|
||||
/// If the `videoDuration` is finite, this method returns it as a `Duration`.
|
||||
/// If the `videoDuration` is `Infinity`, the duration will be
|
||||
/// `-9007199254740990` milliseconds. (See https://github.com/flutter/flutter/issues/107882)
|
||||
/// If the `videoDuration` is `NaN`, this will return null.
|
||||
Duration? convertNumVideoDurationToPluginDuration(num duration) {
|
||||
if (duration.isFinite) {
|
||||
return Duration(
|
||||
milliseconds: (duration * 1000).round(),
|
||||
);
|
||||
} else if (duration.isInfinite) {
|
||||
return jsCompatibleTimeUnset;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, WanJiMi.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'dart:async';
|
||||
import 'dart:html' as html;
|
||||
import 'package:js/js.dart' as js;
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
|
||||
// --------------------------------------------------
|
||||
|
||||
/// {@template hls}
|
||||
///
|
||||
/// HLS
|
||||
/// ---
|
||||
///
|
||||
/// Adds [HLS.js](https://github.com/video-dev/hls.js/) to the HTML document using [html.ScriptElement].
|
||||
///
|
||||
/// {@endtemplate}
|
||||
abstract class HLS {
|
||||
static Future<void> ensureInitialized({String? hls}) {
|
||||
return _lock.synchronized(() async {
|
||||
if (_initialized) {
|
||||
return;
|
||||
}
|
||||
final completer = Completer();
|
||||
try {
|
||||
final script = html.ScriptElement()
|
||||
..async = true
|
||||
..charset = 'utf-8'
|
||||
..type = 'text/javascript'
|
||||
..src = hls ?? kHLSAsset;
|
||||
|
||||
script.onLoad.listen((_) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete();
|
||||
}
|
||||
});
|
||||
script.onError.listen((_) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(Exception('Failed to load HLS.js'));
|
||||
}
|
||||
});
|
||||
|
||||
html.HeadElement? head = html.document.head;
|
||||
if (head == null) {
|
||||
head = html.HeadElement();
|
||||
html.document.append(head);
|
||||
}
|
||||
head.append(script);
|
||||
} catch (_) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(Exception('Failed to load HLS.js'));
|
||||
}
|
||||
}
|
||||
try {
|
||||
await completer.future;
|
||||
_initialized = true;
|
||||
} catch (exception, stacktrace) {
|
||||
print(exception.toString());
|
||||
print(stacktrace.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static const String kHLSAsset =
|
||||
'assets/packages/media_kit/assets/web/hls1.4.10.js';
|
||||
static const String kHLSCDN =
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.4.10/hls.js';
|
||||
|
||||
static final Lock _lock = Lock();
|
||||
static bool _initialized = false;
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
|
||||
@js.JS('Hls.isSupported')
|
||||
external bool isHLSSupported();
|
||||
|
||||
@js.JS()
|
||||
@js.staticInterop
|
||||
class Hls {
|
||||
external factory Hls();
|
||||
}
|
||||
|
||||
extension ExtensionHls on Hls {
|
||||
external void loadSource(String src);
|
||||
external void attachMedia(html.VideoElement video);
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
/// A constant that is true if the application was compiled in release mode.
|
||||
const bool kReleaseMode = bool.fromEnvironment('dart.vm.product');
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
name: media_kit
|
||||
description: A cross-platform video player & audio player for Flutter & Dart. Performant, stable, feature-proof & modular.
|
||||
homepage: https://github.com/media-kit/media-kit
|
||||
repository: https://github.com/media-kit/media-kit
|
||||
version: 1.1.10+1
|
||||
|
||||
topics:
|
||||
- video
|
||||
- video-player
|
||||
- audio
|
||||
- audio-player
|
||||
- cross-platform
|
||||
|
||||
environment:
|
||||
sdk: ">=2.17.0 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
meta: ^1.8.0
|
||||
path: ^1.8.0
|
||||
image: ^4.0.17
|
||||
uri_parser: ^2.0.2
|
||||
collection: ^1.17.0
|
||||
synchronized: ^3.1.0
|
||||
uuid: ">=2.0.0 <5.0.0"
|
||||
http: ">=0.13.0 <2.0.0"
|
||||
safe_local_storage: ^1.0.2
|
||||
universal_platform: ^1.0.0+1
|
||||
js: ^0.6.7
|
||||
|
||||
dev_dependencies:
|
||||
test: ^1.24.1
|
||||
lints: ^2.1.1
|
||||
|
||||
ffigen:
|
||||
name: MPV
|
||||
output: bin/generated/libmpv/bindings.dart
|
||||
headers:
|
||||
entry-points:
|
||||
- headers/client.h
|
||||
dart-bool: true
|
||||
|
||||
flutter:
|
||||
assets:
|
||||
- assets/web/
|
||||
1
packages/media_kit/test/ci/macos/.gitignore
vendored
1
packages/media_kit/test/ci/macos/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
libs
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -e # exit on error
|
||||
set -u # exit on undefined variable
|
||||
|
||||
# relink_dylibs updates the dependency paths of dynamic libraries by replacing a
|
||||
# source prefix with a target prefix
|
||||
relink_dylibs() {
|
||||
SOURCE_PREFIX=$1
|
||||
TARGET_PREFIX=$2
|
||||
DIR=$3
|
||||
|
||||
find $DIR/*.dylib | while read DYLIB; do
|
||||
# sign the dylib because install_name_tool doesn't work on unsigned dylibs
|
||||
codesign --force -s - -v "${DYLIB}" 2>/dev/null
|
||||
|
||||
# change id of current dylib
|
||||
otool -l "$DYLIB" |
|
||||
grep " name " |
|
||||
cut -d " " -f11 |
|
||||
head -n +1 |
|
||||
while read ID; do
|
||||
NAME=$(basename $ID)
|
||||
|
||||
echo "$DYLIB: $NAME"
|
||||
install_name_tool -id "@rpath/$NAME" "$DYLIB" 2>/dev/null
|
||||
done
|
||||
|
||||
# change path of current dependencies
|
||||
otool -l "$DYLIB" |
|
||||
grep " name " |
|
||||
cut -d " " -f11 |
|
||||
tail -n +2 |
|
||||
grep "$SOURCE_PREFIX" |
|
||||
while read DEP; do
|
||||
# libpng16.16.dylib => libpng16.dylib
|
||||
# libxml2.2.dylib => libxml2.dylib
|
||||
DEPNAME="$(basename "$DEP" | sed -r "s|([0-9]+)(\.[0-9]+)*|\1|g")"
|
||||
|
||||
echo "$DYLIB: $DEPNAME"
|
||||
install_name_tool -change "$DEP" \
|
||||
"$TARGET_PREFIX/$DEPNAME" \
|
||||
"$DYLIB" \
|
||||
2>/dev/null
|
||||
done
|
||||
|
||||
# re-sign the dylib, as the change in dependencies invalidates the signature
|
||||
codesign --force -s - -v "${DYLIB}" 2>/dev/null
|
||||
done
|
||||
}
|
||||
|
||||
SOURCE_PREFIX=$1
|
||||
TARGET_PREFIX=$2
|
||||
DIR=$3
|
||||
|
||||
relink_dylibs "$SOURCE_PREFIX" "$TARGET_PREFIX" "$DIR"
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
LIBS_VERSION=v0.6.1
|
||||
|
||||
case $( uname -m ) in
|
||||
x86_64) LIBS_ARCH=amd64;;
|
||||
arm64) LIBS_ARCH=arm64;;
|
||||
*) echo "unsupported arch $( uname -m )" && exit 1;;
|
||||
esac
|
||||
|
||||
rm -rf ./test/ci/macos/libs
|
||||
mkdir -p ./test/ci/macos/libs
|
||||
curl -s -L https://github.com/media-kit/libmpv-darwin-build/releases/download/${LIBS_VERSION}/libmpv-libs_${LIBS_VERSION}_macos-${LIBS_ARCH}-video-default.tar.gz | tar xvz --strip-components 1 - -C ./test/ci/macos/libs
|
||||
|
||||
sh ./test/ci/macos/scripts/relink_dylibs.sh @rpath $PWD/test/ci/macos/libs ./test/ci/macos/libs
|
||||
|
|
@ -1 +0,0 @@
|
|||
export 'sources_native.dart' if (dart.library.html) 'sources_web.dart';
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
final sources = _Sources._();
|
||||
|
||||
class _Sources {
|
||||
_Sources._();
|
||||
|
||||
List<String> get platform => file;
|
||||
|
||||
final List<String> network = [
|
||||
'https://user-images.githubusercontent.com/28951144/229373709-603a7a89-2105-4e1b-a5a5-a6c3567c9a59.mp4',
|
||||
'https://user-images.githubusercontent.com/28951144/229373716-76da0a4e-225a-44e4-9ee7-3e9006dbc3e3.mp4',
|
||||
'https://user-images.githubusercontent.com/28951144/229373718-86ce5e1d-d195-45d5-baa6-ef94041d0b90.mp4',
|
||||
'https://user-images.githubusercontent.com/28951144/229373720-14d69157-1a56-4a78-a2f4-d7a134d7c3e9.mp4',
|
||||
];
|
||||
|
||||
final List<String> file = [];
|
||||
|
||||
final List<Uint8List> bytes = [];
|
||||
|
||||
Future<void> prepare() async {
|
||||
if (_prepared) return;
|
||||
// Download to local [File]s.
|
||||
for (int i = 0; i < network.length; i++) {
|
||||
final destination = path.join(
|
||||
Directory.systemTemp.path,
|
||||
'media_kit',
|
||||
network[i].split('/').last,
|
||||
);
|
||||
if (!await File(destination).parent.exists()) {
|
||||
await File(destination).parent.create(recursive: true);
|
||||
}
|
||||
if (!await File(destination).exists()) {
|
||||
final response = await http.get(Uri.parse(network[i]));
|
||||
await File(destination).writeAsBytes(response.bodyBytes);
|
||||
}
|
||||
file.add(destination);
|
||||
bytes.add(await File(destination).readAsBytes());
|
||||
}
|
||||
// Force forward slashes for Windows.
|
||||
for (int i = 0; i < file.length; i++) {
|
||||
file[i] = file[i].replaceAll(r'\', '/');
|
||||
}
|
||||
_prepared = true;
|
||||
}
|
||||
|
||||
bool _prepared = false;
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
final sources = _Sources._();
|
||||
|
||||
class _Sources {
|
||||
_Sources._();
|
||||
|
||||
List<String> get platform => network;
|
||||
|
||||
final List<String> network = [
|
||||
'https://user-images.githubusercontent.com/28951144/229373709-603a7a89-2105-4e1b-a5a5-a6c3567c9a59.mp4',
|
||||
'https://user-images.githubusercontent.com/28951144/229373716-76da0a4e-225a-44e4-9ee7-3e9006dbc3e3.mp4',
|
||||
'https://user-images.githubusercontent.com/28951144/229373718-86ce5e1d-d195-45d5-baa6-ef94041d0b90.mp4',
|
||||
'https://user-images.githubusercontent.com/28951144/229373720-14d69157-1a56-4a78-a2f4-d7a134d7c3e9.mp4',
|
||||
];
|
||||
|
||||
final List<String> file = [];
|
||||
|
||||
final List<Uint8List> bytes = [];
|
||||
|
||||
Future<void> prepare() => Future.value();
|
||||
}
|
||||
|
|
@ -1,263 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
import 'package:test/test.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import 'package:media_kit/src/models/media/media.dart';
|
||||
|
||||
import '../../../common/sources.dart';
|
||||
|
||||
void main() {
|
||||
setUp(sources.prepare);
|
||||
test(
|
||||
'media-uri-normalization-network',
|
||||
() {
|
||||
for (final source in sources.network) {
|
||||
final test = source;
|
||||
print(test);
|
||||
expect(
|
||||
Media.normalizeURI(source),
|
||||
equals(source),
|
||||
);
|
||||
expect(
|
||||
Media(source).uri,
|
||||
equals(source),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
test(
|
||||
'media-uri-normalization-file',
|
||||
() async {
|
||||
// Path
|
||||
for (final source in sources.file) {
|
||||
final test = source;
|
||||
print(test);
|
||||
expect(
|
||||
Media.normalizeURI(test),
|
||||
equals(source),
|
||||
);
|
||||
expect(
|
||||
Media(test).uri,
|
||||
equals(source),
|
||||
);
|
||||
}
|
||||
// file:// URI
|
||||
for (final source in sources.file) {
|
||||
final test = Uri.file(source).toString();
|
||||
print(test);
|
||||
expect(
|
||||
Media.normalizeURI(test),
|
||||
equals(source),
|
||||
);
|
||||
expect(
|
||||
Media(test).uri,
|
||||
equals(source),
|
||||
);
|
||||
}
|
||||
},
|
||||
skip: UniversalPlatform.isWeb || UniversalPlatform.isWindows,
|
||||
);
|
||||
test(
|
||||
'media-uri-normalization-file',
|
||||
() async {
|
||||
// Path: forward slash separators
|
||||
for (final source in sources.file) {
|
||||
final test = source;
|
||||
print(test);
|
||||
expect(
|
||||
Media.normalizeURI(test),
|
||||
equals(source),
|
||||
);
|
||||
expect(
|
||||
Media(test).uri,
|
||||
equals(source),
|
||||
);
|
||||
}
|
||||
// Path: backwards slash separators
|
||||
for (final source in sources.file) {
|
||||
final test = source.replaceAll('/', r'\');
|
||||
print(test);
|
||||
expect(
|
||||
Media.normalizeURI(test),
|
||||
equals(source),
|
||||
);
|
||||
expect(
|
||||
Media(test).uri,
|
||||
equals(source),
|
||||
);
|
||||
}
|
||||
// file:/// URI
|
||||
for (final source in sources.file) {
|
||||
final test = Uri.file(source).toString();
|
||||
print(test);
|
||||
expect(
|
||||
Media.normalizeURI(test),
|
||||
equals(source),
|
||||
);
|
||||
expect(
|
||||
Media(test).uri,
|
||||
equals(source),
|
||||
);
|
||||
}
|
||||
// file:// URI
|
||||
for (final source in sources.file) {
|
||||
final test =
|
||||
Uri.file(source).toString().replaceAll('file:///', 'file://');
|
||||
print(test);
|
||||
expect(
|
||||
Media.normalizeURI(test),
|
||||
equals(source),
|
||||
);
|
||||
expect(
|
||||
Media(test).uri,
|
||||
equals(source),
|
||||
);
|
||||
}
|
||||
},
|
||||
skip: UniversalPlatform.isWeb || !UniversalPlatform.isWindows,
|
||||
);
|
||||
test(
|
||||
'media-extras-propagate',
|
||||
() {
|
||||
Media.cache.clear();
|
||||
|
||||
final extras = {
|
||||
'foo': 'bar',
|
||||
'baz': 'qux',
|
||||
};
|
||||
|
||||
final a = Media(sources.platform.first, extras: extras);
|
||||
|
||||
// Must have previously defined pre-defined extras.
|
||||
final b = Media(sources.platform.first);
|
||||
|
||||
// Must have newly defined extras.
|
||||
final c = Media(
|
||||
sources.platform.first,
|
||||
extras: {
|
||||
'x': 'y',
|
||||
},
|
||||
);
|
||||
|
||||
print(a.extras);
|
||||
print(b.extras);
|
||||
print(c.extras);
|
||||
|
||||
expect(
|
||||
MapEquality().equals(
|
||||
a.extras,
|
||||
b.extras,
|
||||
),
|
||||
equals(true),
|
||||
);
|
||||
expect(
|
||||
MapEquality().equals(
|
||||
c.extras,
|
||||
{
|
||||
'x': 'y',
|
||||
},
|
||||
),
|
||||
equals(true),
|
||||
);
|
||||
},
|
||||
);
|
||||
test(
|
||||
'media-http-headers-propagate',
|
||||
() {
|
||||
Media.cache.clear();
|
||||
|
||||
final headers = {
|
||||
for (int i = 0; i < 10; i++) 'key_$i': 'value_$i',
|
||||
};
|
||||
|
||||
final a = Media(sources.platform.first, httpHeaders: headers);
|
||||
|
||||
// Must have previously defined pre-defined headers.
|
||||
final b = Media(sources.platform.first);
|
||||
// Must have newly defined headers.
|
||||
final c = Media(
|
||||
sources.platform.first,
|
||||
httpHeaders: {
|
||||
'x': 'y',
|
||||
},
|
||||
);
|
||||
|
||||
print(a.httpHeaders);
|
||||
print(b.httpHeaders);
|
||||
print(c.httpHeaders);
|
||||
|
||||
expect(
|
||||
MapEquality().equals(
|
||||
a.httpHeaders,
|
||||
b.httpHeaders,
|
||||
),
|
||||
equals(true),
|
||||
);
|
||||
expect(
|
||||
MapEquality().equals(
|
||||
c.httpHeaders,
|
||||
{
|
||||
'x': 'y',
|
||||
},
|
||||
),
|
||||
equals(true),
|
||||
);
|
||||
},
|
||||
skip: UniversalPlatform.isWeb,
|
||||
);
|
||||
test(
|
||||
'media-http-headers-propagate',
|
||||
() {
|
||||
Media.cache.clear();
|
||||
|
||||
expect(
|
||||
() => Media(
|
||||
sources.platform.first,
|
||||
httpHeaders: {
|
||||
'x': 'y',
|
||||
},
|
||||
),
|
||||
throwsUnsupportedError,
|
||||
);
|
||||
},
|
||||
skip: !UniversalPlatform.isWeb,
|
||||
);
|
||||
test(
|
||||
'media-finalizer',
|
||||
() async {
|
||||
Media.cache.clear();
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (i == 0) {
|
||||
Media(
|
||||
sources.platform.first,
|
||||
extras: {
|
||||
'foo': 'bar',
|
||||
'baz': 'qux',
|
||||
},
|
||||
httpHeaders: {
|
||||
'key_0': 'value_0',
|
||||
'key_1': 'value_1',
|
||||
},
|
||||
);
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
}
|
||||
if (i == 1) {
|
||||
final playable = Media(sources.platform.first);
|
||||
|
||||
print(playable.extras);
|
||||
print(playable.httpHeaders);
|
||||
|
||||
expect(playable.extras, isNull);
|
||||
expect(playable.httpHeaders, isNull);
|
||||
}
|
||||
}
|
||||
},
|
||||
skip: true,
|
||||
);
|
||||
}
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:media_kit/src/models/track.dart';
|
||||
import 'package:media_kit/src/models/playlist.dart';
|
||||
import 'package:media_kit/src/models/audio_device.dart';
|
||||
import 'package:media_kit/src/models/player_state.dart';
|
||||
import 'package:media_kit/src/models/playlist_mode.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'player-state-default-values',
|
||||
() {
|
||||
final state = PlayerState();
|
||||
expect(
|
||||
state.playlist,
|
||||
equals(Playlist([])),
|
||||
);
|
||||
expect(
|
||||
state.playing,
|
||||
equals(false),
|
||||
);
|
||||
expect(
|
||||
state.completed,
|
||||
equals(false),
|
||||
);
|
||||
expect(
|
||||
state.position,
|
||||
equals(Duration.zero),
|
||||
);
|
||||
expect(
|
||||
state.duration,
|
||||
equals(Duration.zero),
|
||||
);
|
||||
expect(
|
||||
state.volume,
|
||||
equals(100.0),
|
||||
);
|
||||
expect(
|
||||
state.rate,
|
||||
equals(1.0),
|
||||
);
|
||||
expect(
|
||||
state.pitch,
|
||||
equals(1.0),
|
||||
);
|
||||
expect(
|
||||
state.buffering,
|
||||
equals(false),
|
||||
);
|
||||
expect(
|
||||
state.buffer,
|
||||
equals(Duration.zero),
|
||||
);
|
||||
expect(
|
||||
state.playlistMode,
|
||||
equals(PlaylistMode.none),
|
||||
);
|
||||
expect(state.audioParams.format, isNull);
|
||||
expect(state.audioParams.sampleRate, isNull);
|
||||
expect(state.audioParams.channels, isNull);
|
||||
expect(state.audioParams.channelCount, isNull);
|
||||
expect(state.audioParams.hrChannels, isNull);
|
||||
expect(
|
||||
state.audioBitrate,
|
||||
isNull,
|
||||
);
|
||||
expect(state.videoParams.pixelformat, isNull);
|
||||
expect(state.videoParams.hwPixelformat, isNull);
|
||||
expect(state.videoParams.w, isNull);
|
||||
expect(state.videoParams.h, isNull);
|
||||
expect(state.videoParams.dw, isNull);
|
||||
expect(state.videoParams.dh, isNull);
|
||||
expect(state.videoParams.aspect, isNull);
|
||||
expect(state.videoParams.par, isNull);
|
||||
expect(state.videoParams.colormatrix, isNull);
|
||||
expect(state.videoParams.colorlevels, isNull);
|
||||
expect(state.videoParams.primaries, isNull);
|
||||
expect(state.videoParams.gamma, isNull);
|
||||
expect(state.videoParams.sigPeak, isNull);
|
||||
expect(state.videoParams.light, isNull);
|
||||
expect(state.videoParams.chromaLocation, isNull);
|
||||
expect(state.videoParams.rotate, isNull);
|
||||
expect(state.videoParams.stereoIn, isNull);
|
||||
expect(state.videoParams.averageBpp, isNull);
|
||||
expect(state.videoParams.alpha, isNull);
|
||||
expect(
|
||||
state.audioDevice,
|
||||
equals(AudioDevice.auto()),
|
||||
);
|
||||
expect(
|
||||
ListEquality().equals(state.audioDevices, [AudioDevice.auto()]),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
ListEquality().equals(state.audioDevices, [AudioDevice.auto()]),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
state.track,
|
||||
equals(Track()),
|
||||
);
|
||||
expect(
|
||||
state.tracks,
|
||||
equals(Tracks()),
|
||||
);
|
||||
expect(
|
||||
state.track.video,
|
||||
equals(VideoTrack.auto()),
|
||||
);
|
||||
expect(
|
||||
state.track.audio,
|
||||
equals(AudioTrack.auto()),
|
||||
);
|
||||
expect(
|
||||
state.track.subtitle,
|
||||
equals(SubtitleTrack.auto()),
|
||||
);
|
||||
expect(
|
||||
ListEquality().equals(state.tracks.video, [
|
||||
VideoTrack.auto(),
|
||||
VideoTrack.no(),
|
||||
]),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
ListEquality().equals(state.tracks.audio, [
|
||||
AudioTrack.auto(),
|
||||
AudioTrack.no(),
|
||||
]),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
ListEquality().equals(state.tracks.subtitle, [
|
||||
SubtitleTrack.auto(),
|
||||
SubtitleTrack.no(),
|
||||
]),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
state.width,
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
state.height,
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
ListEquality().equals(state.subtitle, ['', '']),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:ffi';
|
||||
import 'dart:async';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:media_kit/ffi/ffi.dart';
|
||||
|
||||
import 'package:media_kit/src/player/native/core/initializer.dart';
|
||||
import 'package:media_kit/src/player/native/core/native_library.dart';
|
||||
|
||||
import 'package:media_kit/generated/libmpv/bindings.dart';
|
||||
|
||||
void main() {
|
||||
setUp(NativeLibrary.ensureInitialized);
|
||||
test(
|
||||
'initializer-create',
|
||||
() {
|
||||
expect(
|
||||
Initializer.create(NativeLibrary.path, (_) async {}),
|
||||
completes,
|
||||
);
|
||||
},
|
||||
);
|
||||
test(
|
||||
'initializer-dispose',
|
||||
() async {
|
||||
final handle = await Initializer.create(NativeLibrary.path, (_) async {});
|
||||
expect(
|
||||
() => Initializer.dispose(handle),
|
||||
returnsNormally,
|
||||
);
|
||||
},
|
||||
);
|
||||
test(
|
||||
'initializer-callback',
|
||||
() async {
|
||||
final mpv = MPV(DynamicLibrary.open(NativeLibrary.path));
|
||||
|
||||
int count = 0;
|
||||
final shutdown = Completer();
|
||||
|
||||
final expectPauseFalse = expectAsync1((value) {
|
||||
print(value);
|
||||
expect(value, isFalse);
|
||||
count++;
|
||||
if (count == 2) {
|
||||
shutdown.complete();
|
||||
}
|
||||
});
|
||||
final expectPauseTrue = expectAsync1((value) {
|
||||
print(value);
|
||||
expect(value, isTrue);
|
||||
count++;
|
||||
if (count == 2) {
|
||||
shutdown.complete();
|
||||
}
|
||||
});
|
||||
final expectShutdown = expectAsync0(() {
|
||||
print('shutdown');
|
||||
expect(true, isTrue);
|
||||
});
|
||||
|
||||
final handle = await Initializer.create(
|
||||
NativeLibrary.path,
|
||||
(event) async {
|
||||
if (event.ref.event_id == mpv_event_id.MPV_EVENT_PROPERTY_CHANGE) {
|
||||
final prop = event.ref.data.cast<mpv_event_property>();
|
||||
if (prop.ref.name.cast<Utf8>().toDartString() == 'pause' &&
|
||||
prop.ref.format == mpv_format.MPV_FORMAT_FLAG) {
|
||||
final value = prop.ref.data.cast<Bool>().value;
|
||||
if (value) {
|
||||
expectPauseTrue(value);
|
||||
}
|
||||
if (!value) {
|
||||
expectPauseFalse(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (event.ref.event_id == mpv_event_id.MPV_EVENT_SHUTDOWN) {
|
||||
expectShutdown();
|
||||
}
|
||||
},
|
||||
);
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
{
|
||||
final name = 'pause'.toNativeUtf8();
|
||||
mpv.mpv_observe_property(
|
||||
handle,
|
||||
0,
|
||||
name.cast(),
|
||||
mpv_format.MPV_FORMAT_FLAG,
|
||||
);
|
||||
calloc.free(name);
|
||||
}
|
||||
{
|
||||
final command = 'cycle pause'.toNativeUtf8();
|
||||
mpv.mpv_command_string(
|
||||
handle,
|
||||
command.cast(),
|
||||
);
|
||||
calloc.free(command);
|
||||
}
|
||||
await shutdown.future;
|
||||
{
|
||||
final command = 'quit 0'.toNativeUtf8();
|
||||
mpv.mpv_command_string(
|
||||
handle,
|
||||
command.cast(),
|
||||
);
|
||||
calloc.free(command);
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
|
||||
Initializer.dispose(handle);
|
||||
},
|
||||
);
|
||||
test(
|
||||
'initializer-options-with-callback',
|
||||
() async {
|
||||
final mpv = MPV(DynamicLibrary.open(NativeLibrary.path));
|
||||
final handle = await Initializer.create(
|
||||
NativeLibrary.path,
|
||||
(_) async {},
|
||||
options: {
|
||||
'config': 'yes',
|
||||
'config-dir': dirname(Platform.script.toFilePath()),
|
||||
},
|
||||
);
|
||||
{
|
||||
final name = 'config'.toNativeUtf8();
|
||||
final value = mpv.mpv_get_property_string(
|
||||
handle,
|
||||
name.cast(),
|
||||
);
|
||||
calloc.free(name);
|
||||
expect(
|
||||
value.cast<Utf8>().toDartString(),
|
||||
'yes',
|
||||
);
|
||||
}
|
||||
{
|
||||
final name = 'config-dir'.toNativeUtf8();
|
||||
final value = mpv.mpv_get_property_string(
|
||||
handle,
|
||||
name.cast(),
|
||||
);
|
||||
calloc.free(name);
|
||||
expect(
|
||||
value.cast<Utf8>().toDartString(),
|
||||
dirname(Platform.script.toFilePath()),
|
||||
);
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
|
||||
Initializer.dispose(handle);
|
||||
},
|
||||
);
|
||||
test(
|
||||
'initializer-options-without-callback',
|
||||
() async {
|
||||
final mpv = MPV(DynamicLibrary.open(NativeLibrary.path));
|
||||
final handle = await Initializer.create(
|
||||
NativeLibrary.path,
|
||||
null,
|
||||
options: {
|
||||
'config': 'yes',
|
||||
'config-dir': dirname(Platform.script.toFilePath()),
|
||||
},
|
||||
);
|
||||
{
|
||||
final name = 'config'.toNativeUtf8();
|
||||
final value = mpv.mpv_get_property_string(
|
||||
handle,
|
||||
name.cast(),
|
||||
);
|
||||
calloc.free(name);
|
||||
expect(
|
||||
value.cast<Utf8>().toDartString(),
|
||||
'yes',
|
||||
);
|
||||
}
|
||||
{
|
||||
final name = 'config-dir'.toNativeUtf8();
|
||||
final value = mpv.mpv_get_property_string(
|
||||
handle,
|
||||
name.cast(),
|
||||
);
|
||||
calloc.free(name);
|
||||
expect(
|
||||
value.cast<Utf8>().toDartString(),
|
||||
dirname(Platform.script.toFilePath()),
|
||||
);
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
|
||||
Initializer.dispose(handle);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
/// This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
///
|
||||
/// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
/// All rights reserved.
|
||||
/// Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:media_kit/src/player/native/core/native_library.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'native-library-ensure-initialized',
|
||||
() {
|
||||
expect(
|
||||
NativeLibrary.ensureInitialized,
|
||||
returnsNormally,
|
||||
);
|
||||
},
|
||||
);
|
||||
test(
|
||||
'native-library-path',
|
||||
() {
|
||||
expect(
|
||||
() {
|
||||
final library = NativeLibrary.path;
|
||||
print(library);
|
||||
},
|
||||
returnsNormally,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:media_kit/src/player/native/utils/asset_loader.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'asset-loader-encode-asset-key',
|
||||
() {
|
||||
expect(
|
||||
AssetLoader.encodeAssetKey('asset://videos/video_0.mp4'),
|
||||
equals('videos/video_0.mp4'),
|
||||
);
|
||||
expect(
|
||||
AssetLoader.encodeAssetKey('asset:///videos/video_0.mp4'),
|
||||
equals('videos/video_0.mp4'),
|
||||
);
|
||||
// Non ASCII characters.
|
||||
expect(
|
||||
AssetLoader.encodeAssetKey('asset://audios/う.wav'),
|
||||
equals('audios/%E3%81%86.wav'),
|
||||
);
|
||||
expect(
|
||||
AssetLoader.encodeAssetKey('asset:///audios/う.wav'),
|
||||
equals('audios/%E3%81%86.wav'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,28 +0,0 @@
|
|||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:media_kit/src/player/web/utils/asset_loader.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'asset-loader-encode-asset-key',
|
||||
() {
|
||||
expect(
|
||||
AssetLoader.encodeAssetKey('asset://videos/video_0.mp4'),
|
||||
equals('videos/video_0.mp4'),
|
||||
);
|
||||
expect(
|
||||
AssetLoader.encodeAssetKey('asset:///videos/video_0.mp4'),
|
||||
equals('videos/video_0.mp4'),
|
||||
);
|
||||
// Non ASCII characters.
|
||||
expect(
|
||||
AssetLoader.encodeAssetKey('asset://audios/う.wav'),
|
||||
equals('audios/%E3%81%86.wav'),
|
||||
);
|
||||
expect(
|
||||
AssetLoader.encodeAssetKey('asset:///audios/う.wav'),
|
||||
equals('audios/%E3%81%86.wav'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
33
packages/media_kit_video/.gitignore
vendored
33
packages/media_kit_video/.gitignore
vendored
|
|
@ -1,33 +0,0 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
||||
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled.
|
||||
|
||||
version:
|
||||
revision: 92857dd0e6878a4b3f4365a0cc420b14fad73911
|
||||
channel: master
|
||||
|
||||
project_type: plugin
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 92857dd0e6878a4b3f4365a0cc420b14fad73911
|
||||
base_revision: 92857dd0e6878a4b3f4365a0cc420b14fad73911
|
||||
- platform: android
|
||||
create_revision: 92857dd0e6878a4b3f4365a0cc420b14fad73911
|
||||
base_revision: 92857dd0e6878a4b3f4365a0cc420b14fad73911
|
||||
- platform: ios
|
||||
create_revision: 92857dd0e6878a4b3f4365a0cc420b14fad73911
|
||||
base_revision: 92857dd0e6878a4b3f4365a0cc420b14fad73911
|
||||
- platform: linux
|
||||
create_revision: 92857dd0e6878a4b3f4365a0cc420b14fad73911
|
||||
base_revision: 92857dd0e6878a4b3f4365a0cc420b14fad73911
|
||||
- platform: macos
|
||||
create_revision: 92857dd0e6878a4b3f4365a0cc420b14fad73911
|
||||
base_revision: 92857dd0e6878a4b3f4365a0cc420b14fad73911
|
||||
- platform: windows
|
||||
create_revision: 92857dd0e6878a4b3f4365a0cc420b14fad73911
|
||||
base_revision: 92857dd0e6878a4b3f4365a0cc420b14fad73911
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
|
|
@ -1,191 +0,0 @@
|
|||
## 1.2.4
|
||||
|
||||
- fix: web compile error
|
||||
|
||||
## 1.2.3
|
||||
|
||||
- feat: `VideoState.update` & `VideoViewParameters`
|
||||
|
||||
## 1.2.2
|
||||
|
||||
- fix: override `setState` & check `mounted` in `MaterialVideoControls` & `MaterialDesktopVideoControls`
|
||||
|
||||
## 1.2.1
|
||||
|
||||
- fix(android): clear `android.view.Surface` before playback
|
||||
- feat: `MaterialVideoControlsThemeData.seekBarAlignment`
|
||||
|
||||
## 1.2.0
|
||||
|
||||
- fix: `MaterialVideoControls` layout
|
||||
|
||||
## 1.1.9
|
||||
|
||||
- fix: unmount `CircularProgressIndicator` buffering indicator if invisible
|
||||
- fix: pass all video attributes to fullscreen route
|
||||
- fix: prevent controls from hiding during seek
|
||||
- fix: hide last video's frame upon `Player.open`
|
||||
- fix: `ThemeData.copyWith` override
|
||||
- fix(android): `hwdec=auto-safe` w/ `enableHardwareAcceleration=true`
|
||||
- fix(windows): fullscreen for non-primary monitors
|
||||
- fix(darwin): `mpv_render_context_free` call
|
||||
- fix(darwin): memory leaks
|
||||
- fix(ios): fix `disposeMPV`
|
||||
- feat: `VideoControllerConfiguration.androidAttachSurfaceAfterVideoParameters`
|
||||
- feat: center the seek-bar within its parent `Container` for improved tap area
|
||||
- feat: `backdropColor` argument in `MaterialVideoControlsThemeData`
|
||||
|
||||
## 1.1.8
|
||||
|
||||
- fix: pass all `*VideoControlsTheme`(s) to fullscreen `context`
|
||||
|
||||
## 1.1.7
|
||||
|
||||
- fix: add `await` to `maybePop` when exiting fullscreen
|
||||
- fix: `MaterialVideoControls`/`MaterialDesktopVideoControls` seekbar glitch
|
||||
- fix(android): S/W rendering fallback
|
||||
- fix(android): create fresh `android.view.Surface` for every video output
|
||||
|
||||
## 1.1.6
|
||||
|
||||
- fix: programmatic fullscreen API
|
||||
- fix(android): pause upon entering fullscreen
|
||||
- fix(android): `waitUntilFirstFrameRenderedNotify` in `FlutterFragmentActivity`
|
||||
|
||||
## 1.1.5
|
||||
|
||||
- fix(android): `waitUntilFirstFrameRenderedNotify` fallback & release-mode
|
||||
|
||||
## 1.1.4
|
||||
|
||||
- feat: `Video` `resumeUponEnteringForegroundMode`
|
||||
- feat(android): `waitUntilFirstFrameRenderedNotify` implementation
|
||||
|
||||
## 1.1.3
|
||||
|
||||
- feat(android): `VideoControllerConfiguration.scale`
|
||||
- fix(android): use `hwdec=auto`
|
||||
- fix(android): `SurfaceTexture.setDefaultBufferSize` & render race
|
||||
|
||||
## 1.1.2
|
||||
|
||||
- fix(windows): memory leak in `GetVideoWidth`/`GetVideoHeight`
|
||||
- fix(linux): `GThread*` leak in S/W render & `video_output_get_(width|height)`
|
||||
- fix(linux): H/W support for multiple videos
|
||||
- build(darwin): bump `mpv` headers to `0.36.0`
|
||||
- build(darwin): use symlinks for `FRAMEWORK_SEARCH_PATHS`, `media_kit_libs_*** >= 1.1.0`
|
||||
- fix(darwin): remove black screen when switching videos ([#332](https://github.com/media-kit/media-kit/issues/332))
|
||||
- feat: `Video`: expose `onEnterFullscreen` & `onExitFullscreen`
|
||||
- feat: feat: `visibleOnMount` `MaterialVideoControls`/`MaterialDesktopVideoControls`
|
||||
- fix: display `bufferingIndicatorBuilder` even if controls are hidden
|
||||
|
||||
## 1.1.1
|
||||
|
||||
- chore: `try`/`catch` native calls to hide stray logs
|
||||
- fix: `MaterialDesktopVideoControls`: do not add `onTapUp` callback if `toggleFullscreenOnDoublePress` is disabled
|
||||
|
||||
## 1.1.0
|
||||
|
||||
- feat: `SubtitleView`, `SubtitleViewConfiguration`
|
||||
- feat: `shiftSubtitlesOnControlsVisibilityChange` in `MaterialVideoControls` & `MaterialDesktopVideoControls`
|
||||
- feat: apply rotation from metadata to video output
|
||||
- feat: improve wakelock behavior
|
||||
- feat: `pauseUponEnteringBackgroundMode`
|
||||
- fix: `bufferingIndicatorBuilder` padding in `MaterialVideoControls` & `MaterialDesktopVideoControls`
|
||||
- fix(windows): maintain aspect ratio in s/w rendering pixel-buffer size clamping
|
||||
- fix(linux): maintain aspect ratio in s/w rendering pixel-buffer size clamping
|
||||
- perf(android): use `hwdec=mediacodec` w/ `enableHardwareAcceleration`
|
||||
- deps: migrate [`package:wakelock_plus`](https://pub.dev/packages/wakelock_plus)
|
||||
|
||||
## 1.0.2
|
||||
|
||||
- fix(video/macos): fix fullscreen support
|
||||
|
||||
## 1.0.1
|
||||
|
||||
- fix: synchronize `VideoController` constructor
|
||||
- fix: `fullscreen` video controls theme data not being applied
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- feat: web support
|
||||
- feat: fullscreen API
|
||||
- feat: acquire wakelock
|
||||
- feat: support for AGP 8.0
|
||||
- feat: pre-built video controls
|
||||
- feat: `controls` argument in `Video` widget
|
||||
- feat: `AdaptiveVideoControls`, `MaterialVideoControls`, `MaterialDesktopVideoControls` & `NoVideoControls`
|
||||
|
||||
## 0.0.12
|
||||
|
||||
- fix(android): improve `Texture` resize handling
|
||||
|
||||
## 0.0.11
|
||||
|
||||
- fix(android): improve `Texture` resize handling
|
||||
|
||||
## 0.0.10
|
||||
|
||||
- feat: `VideoControllerConfiguration`
|
||||
- feat: `VideoController.waitUntilFirstFrameRendered`
|
||||
- refactor: clean-up package structure
|
||||
- refactor: remove `VideoController.dispose`
|
||||
- refactor: `VideoController.create` -> `VideoController` constructor
|
||||
- fix(android): add `av1` to `hwdec-codecs`
|
||||
- fix(android): use `--vo=gpu` + `--hwdec=mediacodec-copy` /w `enableHardwareAcceleration`
|
||||
|
||||
## 0.0.9
|
||||
|
||||
- fix(android): revert to `--vo=mediacodec_embed` in `enableHardwareAcceleration`
|
||||
|
||||
## 0.0.8
|
||||
|
||||
- fix(android): subtitle rendering
|
||||
- fix(android): video rendering inside emulators (#149)
|
||||
- fix(android): video rendering with `enableHardwareAcceleration: false`
|
||||
|
||||
## 0.0.7
|
||||
|
||||
- fix(linux): VAAPI hardware acceleration
|
||||
- perf(windows): `VideoOutput::Resize`: delete texture objects in background
|
||||
|
||||
## 0.0.6
|
||||
|
||||
- fix(windows): synchronize texture object deletion in on unregister _v.i.z_ `VideoOutput::Resize` or `VideoOutput::~VideoOutput`
|
||||
|
||||
## 0.0.5
|
||||
|
||||
- Android support
|
||||
- feat: `VideoController.setSize`
|
||||
- fix: set `vo` to `libmpv` before creating render context
|
||||
- refactor: `VideoController.create` takes `Player` reference instead of `handle`
|
||||
|
||||
## 0.0.4
|
||||
|
||||
- fix: use `mkdir` instead of `.gitkeep`
|
||||
|
||||
## 0.0.3
|
||||
|
||||
- fix: add `.framework` & `.xcframework` for all libs
|
||||
|
||||
## 0.0.2
|
||||
|
||||
- macOS support:
|
||||
- Hardware: MPV_RENDER_API_TYPE_OPENGL + pixel buffer + METAL
|
||||
- Software: MPV_RENDER_API_TYPE_SW + pixel buffer
|
||||
- iOS support:
|
||||
- Hardware: MPV_RENDER_API_TYPE_OPENGL + pixel buffer
|
||||
- Software: MPV_RENDER_API_TYPE_SW + pixel buffer
|
||||
- fix(windows): use `TextureRegistrar::UnregisterTexture` release callback to free texture resources
|
||||
- fix(windows): synchronize texture unregister & release on frame dimensions change
|
||||
- feat: `aspectRatio` parameter for `Video` widget
|
||||
|
||||
## 0.0.1
|
||||
|
||||
- Initial release
|
||||
- Windows support:
|
||||
- Hardware: MPV_RENDER_API_TYPE_OPENGL + ANGLE + DirectX 11
|
||||
- Software: MPV_RENDER_API_TYPE_SW + pixel buffer
|
||||
- GNU/Linux support:
|
||||
- Hardware: MPV_RENDER_API_TYPE_OPENGL + GDK/GL
|
||||
- Software: MPV_RENDER_API_TYPE_SW + pixel buffer
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 & onwards Hitesh Kumar Saini <saini123hitesh@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# [package:media_kit_video](https://github.com/media-kit/media-kit)
|
||||
|
||||
[](https://discord.gg/h7qf2R9n57) [](https://github.com/media-kit/media-kit/actions/workflows/ci.yml)
|
||||
|
||||
Native implementation for video playback in [package:media_kit](https://pub.dev/packages/media_kit).
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2021 & onwards, Hitesh Kumar Saini <<saini123hitesh@gmail.com>>
|
||||
|
||||
This project & the work under this repository is governed by MIT license that can be found in the [LICENSE](./LICENSE) file.
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
9
packages/media_kit_video/android/.gitignore
vendored
9
packages/media_kit_video/android/.gitignore
vendored
|
|
@ -1,9 +0,0 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.cxx
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
group 'com.alexmercerind.media_kit_video'
|
||||
version '1.0'
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.3.0'
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
// Conditional for compatibility with AGP <4.2.
|
||||
if (project.android.hasProperty("namespace")) {
|
||||
namespace 'com.alexmercerind.media_kit_video'
|
||||
}
|
||||
|
||||
compileSdkVersion 31
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
consumerProguardFiles 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
-keepclassmembers class io.flutter.embedding.engine.FlutterEngine {
|
||||
private io.flutter.embedding.engine.FlutterJNI flutterJNI;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
rootProject.name = 'media_kit_video'
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.alexmercerind.media_kit_video">
|
||||
</manifest>
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
/**
|
||||
* This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
* <p>
|
||||
* Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
* All rights reserved.
|
||||
* Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
*/
|
||||
package com.alexmercerind.media_kit_video;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin;
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
|
||||
import io.flutter.plugin.common.MethodChannel.Result;
|
||||
import io.flutter.view.TextureRegistry;
|
||||
|
||||
/**
|
||||
* MediaKitVideoPlugin
|
||||
*/
|
||||
public class MediaKitVideoPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
public static Activity activity;
|
||||
private MethodChannel channel;
|
||||
private TextureRegistry textureRegistry;
|
||||
private VideoOutputManager videoOutputManager;
|
||||
private final Object lock = new Object();
|
||||
|
||||
@Override
|
||||
public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBinding) {
|
||||
synchronized (lock) {
|
||||
MediaKitVideoPlugin.activity = activityPluginBinding.getActivity();
|
||||
|
||||
if (videoOutputManager == null) {
|
||||
if (MediaKitVideoPlugin.activity != null && channel != null && textureRegistry != null) {
|
||||
videoOutputManager = new VideoOutputManager(channel, textureRegistry);
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding activityPluginBinding) {
|
||||
synchronized (lock) {
|
||||
MediaKitVideoPlugin.activity = activityPluginBinding.getActivity();
|
||||
|
||||
if (videoOutputManager == null) {
|
||||
if (MediaKitVideoPlugin.activity != null && channel != null && textureRegistry != null) {
|
||||
videoOutputManager = new VideoOutputManager(channel, textureRegistry);
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromActivityForConfigChanges() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromActivity() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
|
||||
synchronized (lock) {
|
||||
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "com.alexmercerind/media_kit_video");
|
||||
textureRegistry = flutterPluginBinding.getTextureRegistry();
|
||||
|
||||
channel.setMethodCallHandler(this);
|
||||
|
||||
if (videoOutputManager == null) {
|
||||
if (MediaKitVideoPlugin.activity != null && channel != null && textureRegistry != null) {
|
||||
videoOutputManager = new VideoOutputManager(channel, textureRegistry);
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
|
||||
synchronized (lock) {
|
||||
while (videoOutputManager == null) {
|
||||
try {
|
||||
lock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
switch (call.method) {
|
||||
case "VideoOutputManager.Create": {
|
||||
final String handle = call.argument("handle");
|
||||
if (handle != null) {
|
||||
final VideoOutput videoOutput = videoOutputManager.create(Long.parseLong(handle));
|
||||
final HashMap<String, Long> data = new HashMap<>();
|
||||
data.put("id", videoOutput.id);
|
||||
result.success(data);
|
||||
} else {
|
||||
result.success(null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "VideoOutputManager.CreateSurface": {
|
||||
final String handle = call.argument("handle");
|
||||
if (handle != null) {
|
||||
final long wid = videoOutputManager.createSurface(Long.parseLong(handle));
|
||||
final HashMap<String, Long> data = new HashMap<>();
|
||||
data.put("wid", wid);
|
||||
result.success(data);
|
||||
} else {
|
||||
result.success(null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "VideoOutputManager.SetSurfaceTextureSize": {
|
||||
final String handle = call.argument("handle");
|
||||
final String width = call.argument("width");
|
||||
final String height = call.argument("height");
|
||||
if (handle != null) {
|
||||
videoOutputManager.setSurfaceTextureSize(
|
||||
Long.parseLong(handle),
|
||||
Integer.parseInt(Objects.requireNonNull(width)),
|
||||
Integer.parseInt(Objects.requireNonNull(height))
|
||||
);
|
||||
}
|
||||
result.success(null);
|
||||
break;
|
||||
}
|
||||
case "VideoOutputManager.Dispose": {
|
||||
final String handle = call.argument("handle");
|
||||
if (handle != null) {
|
||||
videoOutputManager.dispose(Long.parseLong(handle));
|
||||
}
|
||||
result.success(null);
|
||||
break;
|
||||
}
|
||||
case "Utils.IsEmulator": {
|
||||
result.success(Utils.isEmulator());
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
result.notImplemented();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
|
||||
channel.setMethodCallHandler(null);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
/**
|
||||
* This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
* <p>
|
||||
* Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
* All rights reserved.
|
||||
* Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
*/
|
||||
package com.alexmercerind.media_kit_video;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
public abstract class Utils {
|
||||
public static boolean isEmulator() {
|
||||
try {
|
||||
// https://github.com/fluttercommunity/plus_plugins/blob/ff54dc49230ee5f8b772a3326d4ff3758618df80/packages/device_info_plus/device_info_plus/android/src/main/kotlin/dev/fluttercommunity/plus/device_info/MethodCallHandlerImpl.kt#L110-L125
|
||||
return Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")
|
||||
|| Build.FINGERPRINT.startsWith("generic")
|
||||
|| Build.FINGERPRINT.startsWith("unknown")
|
||||
|| Build.HARDWARE.contains("goldfish")
|
||||
|| Build.HARDWARE.contains("ranchu")
|
||||
|| Build.MODEL.contains("google_sdk")
|
||||
|| Build.MODEL.contains("Emulator")
|
||||
|| Build.MODEL.contains("Android SDK built for x86")
|
||||
|| Build.MANUFACTURER.contains("Genymotion")
|
||||
|| Build.PRODUCT.contains("sdk_google")
|
||||
|| Build.PRODUCT.contains("google_sdk")
|
||||
|| Build.PRODUCT.contains("sdk")
|
||||
|| Build.PRODUCT.contains("sdk_x86")
|
||||
|| Build.PRODUCT.contains("vbox86p")
|
||||
|| Build.PRODUCT.contains("emulator")
|
||||
|| Build.PRODUCT.contains("simulator");
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,248 +0,0 @@
|
|||
/**
|
||||
* This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
* <p>
|
||||
* Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
* All rights reserved.
|
||||
* Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
*/
|
||||
package com.alexmercerind.media_kit_video;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity;
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity;
|
||||
import io.flutter.embedding.android.FlutterView;
|
||||
import io.flutter.embedding.engine.FlutterEngine;
|
||||
import io.flutter.embedding.engine.FlutterJNI;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
import io.flutter.view.TextureRegistry;
|
||||
|
||||
|
||||
public class VideoOutput {
|
||||
public long id = 0;
|
||||
public long wid = 0;
|
||||
|
||||
private Surface surface;
|
||||
private final TextureRegistry.SurfaceTextureEntry surfaceTextureEntry;
|
||||
|
||||
private boolean flutterJNIAPIAvailable;
|
||||
private final Method newGlobalObjectRef;
|
||||
private final Method deleteGlobalObjectRef;
|
||||
private boolean waitUntilFirstFrameRenderedNotify;
|
||||
|
||||
private long handle;
|
||||
private MethodChannel channelReference;
|
||||
private TextureRegistry textureRegistryReference;
|
||||
|
||||
private final Object lock = new Object();
|
||||
|
||||
VideoOutput(long handle, MethodChannel channelReference, TextureRegistry textureRegistryReference) {
|
||||
this.handle = handle;
|
||||
this.channelReference = channelReference;
|
||||
this.textureRegistryReference = textureRegistryReference;
|
||||
try {
|
||||
flutterJNIAPIAvailable = false;
|
||||
waitUntilFirstFrameRenderedNotify = false;
|
||||
// com.alexmercerind.mediakitandroidhelper.MediaKitAndroidHelper is part of package:media_kit_libs_android_video & package:media_kit_libs_android_audio packages.
|
||||
// Use reflection to invoke methods of com.alexmercerind.mediakitandroidhelper.MediaKitAndroidHelper.
|
||||
Class<?> mediaKitAndroidHelperClass = Class.forName("com.alexmercerind.mediakitandroidhelper.MediaKitAndroidHelper");
|
||||
newGlobalObjectRef = mediaKitAndroidHelperClass.getDeclaredMethod("newGlobalObjectRef", Object.class);
|
||||
deleteGlobalObjectRef = mediaKitAndroidHelperClass.getDeclaredMethod("deleteGlobalObjectRef", long.class);
|
||||
newGlobalObjectRef.setAccessible(true);
|
||||
deleteGlobalObjectRef.setAccessible(true);
|
||||
} catch (Throwable e) {
|
||||
Log.i("media_kit", "package:media_kit_libs_android_video missing. Make sure you have added it to pubspec.yaml.");
|
||||
throw new RuntimeException("Failed to initialize com.alexmercerind.media_kit_video.VideoOutput.");
|
||||
}
|
||||
|
||||
surfaceTextureEntry = textureRegistryReference.createSurfaceTexture();
|
||||
|
||||
// If we call setOnFrameAvailableListener after creating SurfaceTextureEntry, the texture won't be displayed inside Flutter UI, because callback set by us will override the Flutter engine's own registered callback:
|
||||
// https://github.com/flutter/engine/blob/f47e864f2dcb9c299a3a3ed22300a1dcacbdf1fe/shell/platform/android/io/flutter/view/FlutterView.java#L942-L958
|
||||
try {
|
||||
if (!flutterJNIAPIAvailable) {
|
||||
flutterJNIAPIAvailable = getFlutterJNIReference() != null;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Log.i("media_kit", String.format(Locale.ENGLISH, "flutterJNIAPIAvailable = %b", flutterJNIAPIAvailable));
|
||||
if (flutterJNIAPIAvailable) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
surfaceTextureEntry.surfaceTexture().setOnFrameAvailableListener((texture) -> {
|
||||
synchronized (lock) {
|
||||
try {
|
||||
if (!waitUntilFirstFrameRenderedNotify) {
|
||||
waitUntilFirstFrameRenderedNotify = true;
|
||||
final HashMap<String, Object> data = new HashMap<>();
|
||||
data.put("handle", handle);
|
||||
channelReference.invokeMethod("VideoOutput.WaitUntilFirstFrameRenderedNotify", data);
|
||||
Log.i("media_kit", String.format(Locale.ENGLISH, "VideoOutput.WaitUntilFirstFrameRenderedNotify = %d", handle));
|
||||
}
|
||||
|
||||
FlutterJNI flutterJNI = null;
|
||||
while (flutterJNI == null) {
|
||||
flutterJNI = getFlutterJNIReference();
|
||||
flutterJNI.markTextureFrameAvailable(id);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}, new Handler());
|
||||
} else {
|
||||
surfaceTextureEntry.surfaceTexture().setOnFrameAvailableListener((texture) -> {
|
||||
synchronized (lock) {
|
||||
try {
|
||||
if (!waitUntilFirstFrameRenderedNotify) {
|
||||
waitUntilFirstFrameRenderedNotify = true;
|
||||
final HashMap<String, Object> data = new HashMap<>();
|
||||
data.put("handle", handle);
|
||||
channelReference.invokeMethod("VideoOutput.WaitUntilFirstFrameRenderedNotify", data);
|
||||
Log.i("media_kit", String.format(Locale.ENGLISH, "VideoOutput.WaitUntilFirstFrameRenderedNotify = %d", handle));
|
||||
}
|
||||
|
||||
FlutterJNI flutterJNI = null;
|
||||
while (flutterJNI == null) {
|
||||
flutterJNI = getFlutterJNIReference();
|
||||
flutterJNI.markTextureFrameAvailable(id);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (!waitUntilFirstFrameRenderedNotify) {
|
||||
waitUntilFirstFrameRenderedNotify = true;
|
||||
final HashMap<String, Object> data = new HashMap<>();
|
||||
data.put("id", id);
|
||||
data.put("wid", wid);
|
||||
data.put("handle", handle);
|
||||
channelReference.invokeMethod("VideoOutput.WaitUntilFirstFrameRenderedNotify", data);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
id = surfaceTextureEntry.id();
|
||||
Log.i("media_kit", String.format(Locale.ENGLISH, "com.alexmercerind.media_kit_video.VideoOutput: id = %d", id));
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
try {
|
||||
surfaceTextureEntry.release();
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
surface.release();
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
final Handler handler = new Handler(Looper.getMainLooper());
|
||||
handler.postDelayed(() -> {
|
||||
try {
|
||||
// Invoke DeleteGlobalRef after a voluntary delay to eliminate possibility of libmpv referencing it sometime in the near future.
|
||||
deleteGlobalObjectRef.invoke(null, wid);
|
||||
Log.i("media_kit", String.format(Locale.ENGLISH, "com.alexmercerind.mediakitandroidhelper.MediaKitAndroidHelper.deleteGlobalObjectRef: %d", wid));
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}, 5000);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public long createSurface() {
|
||||
synchronized (lock) {
|
||||
// Delete previous android.view.Surface & object reference.
|
||||
try {
|
||||
if (surface != null) {
|
||||
clearSurface();
|
||||
surface.release();
|
||||
surface = null;
|
||||
}
|
||||
if (wid != 0) {
|
||||
deleteGlobalObjectRef.invoke(null, wid);
|
||||
wid = 0;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
// Create new android.view.Surface & object reference.
|
||||
try {
|
||||
surface = new Surface(surfaceTextureEntry.surfaceTexture());
|
||||
wid = (long) newGlobalObjectRef.invoke(null, surface);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return wid;
|
||||
}
|
||||
}
|
||||
|
||||
public void setSurfaceTextureSize(int width, int height) {
|
||||
try {
|
||||
surfaceTextureEntry.surfaceTexture().setDefaultBufferSize(width, height);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void clearSurface() {
|
||||
try {
|
||||
final Canvas canvas = surface.lockCanvas(null);
|
||||
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||
surface.unlockCanvasAndPost(canvas);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private FlutterJNI getFlutterJNIReference() {
|
||||
try {
|
||||
FlutterView view = null;
|
||||
// io.flutter.embedding.android.FlutterActivity
|
||||
if (view == null) {
|
||||
view = MediaKitVideoPlugin.activity.findViewById(FlutterActivity.FLUTTER_VIEW_ID);
|
||||
}
|
||||
// io.flutter.embedding.android.FlutterFragmentActivity
|
||||
if (view == null) {
|
||||
final FrameLayout layout = (FrameLayout) MediaKitVideoPlugin.activity.findViewById(FlutterFragmentActivity.FRAGMENT_CONTAINER_ID);
|
||||
for (int i = 0; i < layout.getChildCount(); i++) {
|
||||
final View child = layout.getChildAt(i);
|
||||
if (child instanceof FlutterView) {
|
||||
view = (FlutterView) child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
final FlutterEngine engine = view.getAttachedFlutterEngine();
|
||||
final Field field = engine.getClass().getDeclaredField("flutterJNI");
|
||||
field.setAccessible(true);
|
||||
return (FlutterJNI) field.get(engine);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
/**
|
||||
* This file is a part of media_kit (https://github.com/media-kit/media-kit).
|
||||
* <p>
|
||||
* Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
|
||||
* All rights reserved.
|
||||
* Use of this source code is governed by MIT license that can be found in the LICENSE file.
|
||||
*/
|
||||
package com.alexmercerind.media_kit_video;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
import io.flutter.view.TextureRegistry;
|
||||
|
||||
public class VideoOutputManager {
|
||||
private final HashMap<Long, VideoOutput> videoOutputs = new HashMap<>();
|
||||
private final MethodChannel channelReference;
|
||||
private final TextureRegistry textureRegistryReference;
|
||||
private final Object lock = new Object();
|
||||
|
||||
VideoOutputManager(MethodChannel channelReference, TextureRegistry textureRegistryReference) {
|
||||
this.channelReference = channelReference;
|
||||
this.textureRegistryReference = textureRegistryReference;
|
||||
}
|
||||
|
||||
public VideoOutput create(long handle) {
|
||||
synchronized (lock) {
|
||||
Log.i("media_kit", String.format(Locale.ENGLISH, "com.alexmercerind.media_kit_video.VideoOutputManager.create: %d", handle));
|
||||
if (!videoOutputs.containsKey(handle)) {
|
||||
final VideoOutput videoOutput = new VideoOutput(handle, channelReference, textureRegistryReference);
|
||||
videoOutputs.put(handle, videoOutput);
|
||||
}
|
||||
return videoOutputs.get(handle);
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose(long handle) {
|
||||
synchronized (lock) {
|
||||
Log.i("media_kit", String.format(Locale.ENGLISH, "com.alexmercerind.media_kit_video.VideoOutputManager.dispose: %d", handle));
|
||||
if (videoOutputs.containsKey(handle)) {
|
||||
Objects.requireNonNull(videoOutputs.get(handle)).dispose();
|
||||
videoOutputs.remove(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public long createSurface(long handle) {
|
||||
synchronized (lock) {
|
||||
Log.i("media_kit", String.format(Locale.ENGLISH, "com.alexmercerind.media_kit_video.VideoOutputManager.createSurface: %d", handle));
|
||||
if (videoOutputs.containsKey(handle)) {
|
||||
return Objects.requireNonNull(videoOutputs.get(handle)).createSurface();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void setSurfaceTextureSize(long handle, int width, int height) {
|
||||
synchronized (lock) {
|
||||
Log.i("media_kit", String.format(Locale.ENGLISH, "com.alexmercerind.media_kit_video.VideoOutputManager.setSurfaceTextureSize: %d %d %d", handle, width, height));
|
||||
if (videoOutputs.containsKey(handle)) {
|
||||
Objects.requireNonNull(videoOutputs.get(handle)).setSurfaceTextureSize(width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
.cache
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
public enum MPVHelpers {
|
||||
public static func checkError(_ status: CInt) {
|
||||
if status < 0 {
|
||||
NSLog("MPVHelpers: error: \(String(cString: mpv_error_string(status)))")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
public static func getVideoOutParams(
|
||||
_ handle: OpaquePointer
|
||||
) -> MPVVideoOutParams {
|
||||
var node = mpv_node()
|
||||
defer {
|
||||
mpv_free_node_contents(&node)
|
||||
}
|
||||
|
||||
mpv_get_property(handle, "video-out-params", MPV_FORMAT_NODE, &node)
|
||||
|
||||
if node.format != MPV_FORMAT_NODE_MAP {
|
||||
return MPVVideoOutParams.empty
|
||||
}
|
||||
|
||||
let map: mpv_node_list = node.u.list!.pointee
|
||||
if map.num == 0 {
|
||||
return MPVVideoOutParams.empty
|
||||
}
|
||||
|
||||
return MPVVideoOutParams.fromMPVNodeList(map)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
public class MPVVideoOutParams {
|
||||
public let dw: Int64
|
||||
public let dh: Int64
|
||||
public let rotate: Int64
|
||||
|
||||
init(
|
||||
dw: Int64,
|
||||
dh: Int64,
|
||||
rotate: Int64
|
||||
) {
|
||||
self.dw = dw
|
||||
self.dh = dh
|
||||
self.rotate = rotate
|
||||
}
|
||||
|
||||
public static let empty = MPVVideoOutParams(
|
||||
dw: 0,
|
||||
dh: 0,
|
||||
rotate: 0
|
||||
)
|
||||
|
||||
public static func fromMPVNodeList(
|
||||
_ map: mpv_node_list
|
||||
) -> MPVVideoOutParams {
|
||||
var dw: Int64 = 0
|
||||
var dh: Int64 = 0
|
||||
var rotate: Int64 = 0
|
||||
|
||||
var kptr = map.keys!
|
||||
var vptr = map.values!
|
||||
for _ in 0 ..< map.num {
|
||||
let key = String(cString: kptr.pointee!)
|
||||
let value: mpv_node = vptr.pointee
|
||||
|
||||
kptr = kptr.successor()
|
||||
vptr = vptr.successor()
|
||||
|
||||
if value.format == MPV_FORMAT_INT64 {
|
||||
if key == "dw" {
|
||||
dw = value.u.int64
|
||||
} else if key == "dh" {
|
||||
dh = value.u.int64
|
||||
} else if key == "rotate" {
|
||||
rotate = value.u.int64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MPVVideoOutParams(
|
||||
dw: dw,
|
||||
dh: dh,
|
||||
rotate: rotate
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
#if canImport(Flutter)
|
||||
import Flutter
|
||||
#elseif canImport(FlutterMacOS)
|
||||
import FlutterMacOS
|
||||
#endif
|
||||
|
||||
public class MediaKitVideoPlugin: NSObject, FlutterPlugin {
|
||||
private static let CHANNEL_NAME = "com.alexmercerind/media_kit_video"
|
||||
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
#if canImport(Flutter)
|
||||
let binaryMessenger = registrar.messenger()
|
||||
let registry = registrar.textures()
|
||||
let utils: UtilsProtocol? = nil
|
||||
#elseif canImport(FlutterMacOS)
|
||||
let binaryMessenger = registrar.messenger
|
||||
let registry = registrar.textures
|
||||
let utils: UtilsProtocol? = Utils(registrar)
|
||||
#endif
|
||||
|
||||
let channel = FlutterMethodChannel(
|
||||
name: CHANNEL_NAME,
|
||||
binaryMessenger: binaryMessenger
|
||||
)
|
||||
let instance = MediaKitVideoPlugin(
|
||||
registry: registry,
|
||||
channel: channel,
|
||||
utils: utils
|
||||
)
|
||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||
}
|
||||
|
||||
private let channel: FlutterMethodChannel
|
||||
private let videoOutputManager: VideoOutputManager
|
||||
private let utils: UtilsProtocol?
|
||||
|
||||
init(
|
||||
registry: FlutterTextureRegistry,
|
||||
channel: FlutterMethodChannel,
|
||||
utils: UtilsProtocol?
|
||||
) {
|
||||
self.channel = channel
|
||||
videoOutputManager = VideoOutputManager(
|
||||
registry: registry
|
||||
)
|
||||
self.utils = utils
|
||||
}
|
||||
|
||||
public func handle(
|
||||
_ call: FlutterMethodCall,
|
||||
result: @escaping FlutterResult
|
||||
) {
|
||||
switch call.method {
|
||||
case "VideoOutputManager.Create":
|
||||
handleCreateMethodCall(call.arguments, result)
|
||||
case "VideoOutputManager.SetSize":
|
||||
handleSetSizeMethodCall(call.arguments, result)
|
||||
case "VideoOutputManager.Dispose":
|
||||
handleDisposeMethodCall(call.arguments, result)
|
||||
case "Utils.EnterNativeFullscreen":
|
||||
handleEnterNativeFullscreenMethodCall(call.arguments, result)
|
||||
case "Utils.ExitNativeFullscreen":
|
||||
handleExitNativeFullscreenMethodCall(call.arguments, result)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleCreateMethodCall(
|
||||
_ arguments: Any?,
|
||||
_ result: FlutterResult
|
||||
) {
|
||||
let args = arguments as? [String: Any]
|
||||
let handleStr = args?["handle"] as! String
|
||||
let handle: Int64? = Int64(handleStr)
|
||||
let configDict = args?["configuration"] as! [String: Any]
|
||||
let configuration = VideoOutputConfiguration.fromDict(configDict)
|
||||
|
||||
assert(handle != nil, "handle must be an Int64")
|
||||
|
||||
videoOutputManager.create(
|
||||
handle: handle!,
|
||||
configuration: configuration,
|
||||
textureUpdateCallback: { (_ textureId: Int64, _ size: CGSize) in
|
||||
self.channel.invokeMethod(
|
||||
"VideoOutput.Resize",
|
||||
arguments: [
|
||||
"handle": handle!,
|
||||
"id": textureId,
|
||||
"rect": [
|
||||
"top": 0,
|
||||
"left": 0,
|
||||
"width": size.width,
|
||||
"height": size.height,
|
||||
],
|
||||
] as [String: Any]
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
result(nil)
|
||||
}
|
||||
|
||||
private func handleSetSizeMethodCall(
|
||||
_ arguments: Any?,
|
||||
_ result: FlutterResult
|
||||
) {
|
||||
let args = arguments as? [String: Any]
|
||||
let handleStr = args?["handle"] as! String
|
||||
let widthStr = args?["width"] as! String
|
||||
let heightStr = args?["height"] as! String
|
||||
|
||||
let handle: Int64? = Int64(handleStr)
|
||||
let width: Int64? = Int64(widthStr)
|
||||
let height: Int64? = Int64(heightStr)
|
||||
|
||||
assert(handle != nil, "handle must be an Int64")
|
||||
|
||||
self.videoOutputManager.setSize(
|
||||
handle: handle!,
|
||||
width: width,
|
||||
height: height
|
||||
)
|
||||
|
||||
result(nil)
|
||||
}
|
||||
|
||||
private func handleDisposeMethodCall(
|
||||
_ arguments: Any?,
|
||||
_ result: FlutterResult
|
||||
) {
|
||||
let args = arguments as? [String: Any]
|
||||
let handleStr = args?["handle"] as! String
|
||||
let handle: Int64? = Int64(handleStr)
|
||||
|
||||
assert(handle != nil, "handle must be an Int64")
|
||||
|
||||
videoOutputManager.destroy(
|
||||
handle: handle!
|
||||
)
|
||||
|
||||
result(nil)
|
||||
}
|
||||
|
||||
private func handleEnterNativeFullscreenMethodCall(
|
||||
_: Any?,
|
||||
_ result: FlutterResult
|
||||
) {
|
||||
if utils == nil {
|
||||
return result(FlutterMethodNotImplemented)
|
||||
}
|
||||
|
||||
utils?.enterNativeFullscreen()
|
||||
result(nil)
|
||||
}
|
||||
|
||||
private func handleExitNativeFullscreenMethodCall(
|
||||
_: Any?,
|
||||
_ result: FlutterResult
|
||||
) {
|
||||
if utils == nil {
|
||||
return result(FlutterMethodNotImplemented)
|
||||
}
|
||||
|
||||
utils?.exitNativeFullscreen()
|
||||
result(nil)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
#if canImport(Flutter)
|
||||
import Flutter
|
||||
#elseif canImport(FlutterMacOS)
|
||||
import FlutterMacOS
|
||||
#endif
|
||||
|
||||
public protocol ResizableTextureProtocol: NSObject, FlutterTexture {
|
||||
func resize(_ size: CGSize)
|
||||
func render(_ size: CGSize)
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
#if canImport(Flutter)
|
||||
import Flutter
|
||||
#elseif canImport(FlutterMacOS)
|
||||
import FlutterMacOS
|
||||
#endif
|
||||
|
||||
// This class avoids data race when called from a thread
|
||||
public class SafeResizableTexture:
|
||||
NSObject,
|
||||
FlutterTexture,
|
||||
ResizableTextureProtocol
|
||||
{
|
||||
private let lock = NSRecursiveLock()
|
||||
private let child: ResizableTextureProtocol
|
||||
|
||||
init(_ child: ResizableTextureProtocol) {
|
||||
self.child = child
|
||||
}
|
||||
|
||||
public func resize(_ size: CGSize) {
|
||||
return locked {
|
||||
return child.resize(size)
|
||||
}
|
||||
}
|
||||
|
||||
public func render(_ size: CGSize) {
|
||||
return locked {
|
||||
return child.render(size)
|
||||
}
|
||||
}
|
||||
|
||||
public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
|
||||
return child.copyPixelBuffer()
|
||||
}
|
||||
|
||||
private func locked<T>(do block: () -> T) -> T {
|
||||
lock.lock()
|
||||
defer {
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
return block()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue