diff --git a/flutter-exoplayer/flutter_beta_exoplayer_creator/README.md b/flutter-exoplayer/flutter_beta_exoplayer_creator/README.md index a020792..11ac461 100644 --- a/flutter-exoplayer/flutter_beta_exoplayer_creator/README.md +++ b/flutter-exoplayer/flutter_beta_exoplayer_creator/README.md @@ -1,6 +1,6 @@ -# flutter_exoplayer_creator +# media3_exoplayer_creator -Flutter version of ExoPlayer Creator +ExoPlayer Creator that supports Media3 ## Getting Started diff --git a/flutter-exoplayer/flutter_beta_exoplayer_creator/android/app/src/main/AndroidManifest.xml b/flutter-exoplayer/flutter_beta_exoplayer_creator/android/app/src/main/AndroidManifest.xml index 240a05c..7fe6dc0 100644 --- a/flutter-exoplayer/flutter_beta_exoplayer_creator/android/app/src/main/AndroidManifest.xml +++ b/flutter-exoplayer/flutter_beta_exoplayer_creator/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + diff --git a/flutter-exoplayer/flutter_beta_exoplayer_creator/lib/main.dart b/flutter-exoplayer/flutter_beta_exoplayer_creator/lib/main.dart index 2214a28..b06ff22 100644 --- a/flutter-exoplayer/flutter_beta_exoplayer_creator/lib/main.dart +++ b/flutter-exoplayer/flutter_beta_exoplayer_creator/lib/main.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:video_player/video_player.dart'; -import 'package:chewie/chewie.dart'; -import 'package:keep_screen_on/keep_screen_on.dart'; // Import the keep_screen_on package +import 'package:media3_exoplayer_creator/screens/video_screen.dart'; // Import other files as needed void main() { runApp(MyApp()); @@ -15,162 +13,43 @@ class MyApp extends StatelessWidget { return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData( - primarySwatch: Colors.blue, // Set primary color to blue - colorScheme: ColorScheme.light( - primary: Colors.blue, // Primary color - secondary: Colors.blueAccent, // Accent color (replaces accentColor) + primarySwatch: Colors.blue, // Set primary color to blue + colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue).copyWith( + secondary: Colors.blue, // Set accent (secondary) color to blue accent ), appBarTheme: AppBarTheme( - backgroundColor: Colors.blue, // Set AppBar background to blue - ), - buttonTheme: ButtonThemeData( - buttonColor: Colors.blue, // Set button color to blue - ), - // Customize other theme properties as needed - ), - home: VideoURLScreen(), - ); - } -} - -class VideoURLScreen extends StatefulWidget { - const VideoURLScreen({super.key}); - - @override - _VideoURLScreenState createState() => _VideoURLScreenState(); -} - -class _VideoURLScreenState extends State { - String _videoUrl = ''; - - // Show the dialog to ask for the video URL - Future _showVideoURLDialog() async { - final TextEditingController videoController = TextEditingController(); - - return showDialog( - context: context, - barrierDismissible: false, // Prevent dismissing by tapping outside - builder: (BuildContext context) { - return AlertDialog( - title: Text('Enter Video URL'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: videoController, - decoration: InputDecoration(hintText: 'Enter a valid video URL'), - keyboardType: TextInputType.url, - ), - ], + color: Colors.blue, // Set the AppBar color to blue + titleTextStyle: TextStyle( + color: Colors.white, // Set text color to white for AppBar + fontSize: 20, + fontWeight: FontWeight.bold, ), - actions: [ - TextButton( - child: Text('Cancel'), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: Text('OK'), - onPressed: () { - setState(() { - _videoUrl = videoController.text; - }); - Navigator.of(context).pop(); - }, - ), - ], - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('ExoPlayer Creator (New Edition)'), - ), - body: Center( - child: _videoUrl.isEmpty - ? ElevatedButton( - onPressed: _showVideoURLDialog, - child: Text('Enter Video URL'), - ) - : VideoPlayerWidget(videoUrl: _videoUrl), - ), - ); - } -} - -class VideoPlayerWidget extends StatefulWidget { - final String videoUrl; - - const VideoPlayerWidget({super.key, required this.videoUrl}); - - @override - _VideoPlayerWidgetState createState() => _VideoPlayerWidgetState(); -} - -class _VideoPlayerWidgetState extends State { - late VideoPlayerController _videoPlayerController; - late ChewieController _chewieController; - - @override - void initState() { - super.initState(); - _videoPlayerController = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); // Convert String to Uri - - _chewieController = ChewieController( - videoPlayerController: _videoPlayerController, - aspectRatio: 16 / 9, - autoPlay: true, - looping: true, - ); - - // Enable keep_screen_on to prevent screen sleep - _videoPlayerController.addListener(() { - if (_videoPlayerController.value.isPlaying) { - KeepScreenOn.turnOn(); // Keep the screen on while video is playing - } else { - KeepScreenOn.turnOff(); // Allow the screen to turn off when the video is paused or stopped - } - }); - } - - @override - void dispose() { - super.dispose(); - _chewieController.dispose(); - _videoPlayerController.dispose(); - KeepScreenOn.turnOff(); // Ensure screen turns off when the widget is disposed - } - - @override - Widget build(BuildContext context) { - return Chewie( - controller: _chewieController, - ); - } -} - -// Confirm exit when back is pressed -Future _onWillPop(BuildContext context) async { - return await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Are you sure?'), - content: Text('If you exit now, ExoPlayer will stop playing.'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text('No'), ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text('Yes'), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: Colors.blueAccent, // Set text color to blue for TextButtons + ), ), - ], - ), - ) ?? false; + inputDecorationTheme: InputDecorationTheme( + filled: true, // Allow filled background for text fields + fillColor: Colors.white, // White background for text fields + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), // Rounded corners for text fields + borderSide: BorderSide( + color: Colors.blueAccent, // Border color + width: 2, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: Colors.blueAccent, // Focused border color + width: 2, + ), + ), + ), + ), + home: VideoScreen(), + ); + } } diff --git a/flutter-exoplayer/flutter_beta_exoplayer_creator/lib/screens/video_screen.dart b/flutter-exoplayer/flutter_beta_exoplayer_creator/lib/screens/video_screen.dart new file mode 100644 index 0000000..45a5d6a --- /dev/null +++ b/flutter-exoplayer/flutter_beta_exoplayer_creator/lib/screens/video_screen.dart @@ -0,0 +1,176 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:http/http.dart' as http; +import 'package:keep_screen_on/keep_screen_on.dart'; +import 'package:permission_handler/permission_handler.dart'; // Add this import +import '../widgets/video_player_widget.dart'; + +// The VideoScreen widget class +class VideoScreen extends StatefulWidget { + const VideoScreen({super.key}); + + @override + _VideoScreenState createState() => _VideoScreenState(); +} + +// The State class for VideoScreen +class _VideoScreenState extends State { + String _videoUrl = ''; + String _filePath = ''; + String _subtitleUrl = ''; // Subtitle URL variable + String _subtitleFilePath = ''; // Subtitle file path + + // Method to show video URL dialog + Future _showVideoURLDialog() async { + final TextEditingController videoController = TextEditingController(); + + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: Text('Enter Video URL'), + content: TextField( + controller: videoController, + decoration: InputDecoration(hintText: 'Enter a valid video URL'), + keyboardType: TextInputType.url, + ), + actions: [ + TextButton( + child: Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text('OK'), + onPressed: () { + setState(() { + _videoUrl = videoController.text; + _filePath = ''; + }); + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + + // Method to pick video file + Future _pickFile() async { + FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.video); + + if (result != null && result.files.single.path != null) { + setState(() { + _filePath = result.files.single.path!; + _videoUrl = ''; + }); + } + } + + // Method to pick subtitle file + Future _pickSubtitleFile() async { + FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['vtt']); + + if (result != null && result.files.single.path != null) { + setState(() { + _subtitleFilePath = result.files.single.path!; + _subtitleUrl = ''; // Clear subtitle URL when a subtitle file is picked + }); + } + } + + // Method to enter subtitle URL + Future _enterSubtitleURL() async { + final TextEditingController subtitleController = TextEditingController(); + + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: Text('Enter Subtitle URL'), + content: TextField( + controller: subtitleController, + decoration: InputDecoration(hintText: 'Enter a valid subtitle URL'), + keyboardType: TextInputType.url, + ), + actions: [ + TextButton( + child: Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text('OK'), + onPressed: () { + setState(() { + _subtitleUrl = subtitleController.text; + }); + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: _videoUrl.isEmpty && _filePath.isEmpty + ? AppBar( + title: Text('ExoPlayer Creator'), + ) + : null, // Hide AppBar when video is playing + body: Center( + child: _videoUrl.isEmpty && _filePath.isEmpty + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: _showVideoURLDialog, + child: Text('Enter Video URL'), + ), + SizedBox(width: 16), + ElevatedButton( + onPressed: _pickFile, + child: Text('Choose Video File'), + ), + ], + ), + SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: _enterSubtitleURL, + child: Text('Enter Subtitle URL'), + ), + SizedBox(width: 16), + ElevatedButton( + onPressed: _pickSubtitleFile, + child: Text('Choose Subtitle File'), + ), + ], + ), + ], + ) + : VideoPlayerWidget( + videoUrl: _videoUrl, + filePath: _filePath, + subtitleUrl: _subtitleUrl, // Pass subtitle URL to VideoPlayerWidget + subtitleFilePath: _subtitleFilePath, // Pass subtitle file path to VideoPlayerWidget + ), + ), + ); + } +} \ No newline at end of file diff --git a/flutter-exoplayer/flutter_beta_exoplayer_creator/lib/utils/permission_utils.dart b/flutter-exoplayer/flutter_beta_exoplayer_creator/lib/utils/permission_utils.dart new file mode 100644 index 0000000..8a60c6e --- /dev/null +++ b/flutter-exoplayer/flutter_beta_exoplayer_creator/lib/utils/permission_utils.dart @@ -0,0 +1,52 @@ +import 'package:permission_handler/permission_handler.dart'; +import 'package:flutter/material.dart'; + +Future requestPermissionIfNeeded(String subtitleFilePath, BuildContext context) async { + if (subtitleFilePath.isNotEmpty) { + // Only request permission if a subtitle file is chosen + PermissionStatus status = await Permission.storage.status; + + if (!status.isGranted) { + // If permission is not granted, request it + status = await Permission.storage.request(); + if (status.isDenied) { + // If permission is denied, show an alert dialog + _showPermissionDeniedDialog(context); + return; // Exit as we cannot proceed without permission + } else if (status.isPermanentlyDenied) { + // If the permission is permanently denied, guide the user to settings + _showPermissionDeniedDialog(context, permanentlyDenied: true); + return; + } + } + } +} + +void _showPermissionDeniedDialog(BuildContext context, {bool permanentlyDenied = false}) { + showDialog( + context: context, + barrierDismissible: false, // Prevent tapping outside to dismiss + builder: (BuildContext context) { + return AlertDialog( + title: Text('Permission Denied'), + content: Text( + permanentlyDenied + ? 'The permission to access external storage has been permanently denied. Please go to the app settings to enable it.' + : 'You have denied the permission to access external storage. Please allow it to proceed.', + ), + actions: [ + TextButton( + onPressed: () { + if (permanentlyDenied) { + // Optionally, open app settings if permission is permanently denied + openAppSettings(); + } + Navigator.of(context).pop(); // Close the dialog + }, + child: Text('OK'), + ), + ], + ); + }, + ); +} diff --git a/flutter-exoplayer/flutter_beta_exoplayer_creator/lib/utils/web_vtt.dart b/flutter-exoplayer/flutter_beta_exoplayer_creator/lib/utils/web_vtt.dart new file mode 100644 index 0000000..5f7e015 --- /dev/null +++ b/flutter-exoplayer/flutter_beta_exoplayer_creator/lib/utils/web_vtt.dart @@ -0,0 +1,36 @@ +class WebVttCue { + final Duration start; + final Duration end; + final String text; + + WebVttCue({ + required this.start, + required this.end, + required this.text, + }); +} + +List parseWebVtt(String subtitleData) { + final cuePattern = RegExp(r'(\d{2}:\d{2}:\d{2}.\d{3}) --> (\d{2}:\d{2}:\d{2}.\d{3})\n(.*?)\n\n', dotAll: true); + final List cues = []; + + for (final match in cuePattern.allMatches(subtitleData)) { + final start = parseTime(match.group(1)!); + final end = parseTime(match.group(2)!); + final text = match.group(3)!; + cues.add(WebVttCue(start: start, end: end, text: text)); + } + + return cues; +} + +Duration parseTime(String time) { + final parts = time.split(':'); + final secondsParts = parts[2].split('.'); + return Duration( + hours: int.parse(parts[0]), + minutes: int.parse(parts[1]), + seconds: int.parse(secondsParts[0]), + milliseconds: int.parse(secondsParts[1]), + ); +} diff --git a/flutter-exoplayer/flutter_beta_exoplayer_creator/lib/widgets/video_player_widget.dart b/flutter-exoplayer/flutter_beta_exoplayer_creator/lib/widgets/video_player_widget.dart new file mode 100644 index 0000000..63ff0e4 --- /dev/null +++ b/flutter-exoplayer/flutter_beta_exoplayer_creator/lib/widgets/video_player_widget.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; +import 'package:chewie/chewie.dart'; +import 'dart:io'; // Import to use File class +import 'package:media3_exoplayer_creator/utils/permission_utils.dart'; // Import permission_utils.dart for permission handling +import 'package:media3_exoplayer_creator/utils/web_vtt.dart'; + + + +class VideoPlayerWidget extends StatefulWidget { + final String videoUrl; + final String filePath; + final String subtitleUrl; + final String subtitleFilePath; + + const VideoPlayerWidget({ + Key? key, + required this.videoUrl, + required this.filePath, + required this.subtitleUrl, + required this.subtitleFilePath, + }) : super(key: key); + + @override + _VideoPlayerWidgetState createState() => _VideoPlayerWidgetState(); +} + +class _VideoPlayerWidgetState extends State { + late VideoPlayerController _videoPlayerController; + late ChewieController _chewieController; + late List _subtitles; + String? _currentSubtitle; + + @override + void initState() { + super.initState(); + + // Request permission for subtitle files if a subtitle file path is provided + if (widget.subtitleFilePath.isNotEmpty) { + requestPermissionIfNeeded(widget.subtitleFilePath, context); // Correctly call the method here + } + + // Initialize the video player controller based on video URL or file path + if (widget.filePath.isNotEmpty) { + _videoPlayerController = VideoPlayerController.file(File(widget.filePath)); + } else { + _videoPlayerController = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); + } + + // Initialize the Chewie controller for video playback + _chewieController = ChewieController( + videoPlayerController: _videoPlayerController, + aspectRatio: 16 / 9, + autoPlay: true, + looping: true, + ); + + // Load subtitles if needed + _loadSubtitles(); + } + + // Method to load subtitles (you can adapt it to your subtitle parsing logic) + Future _loadSubtitles() async { + // Add your subtitle loading logic here + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + Chewie(controller: _chewieController), // Video player widget + if (_currentSubtitle != null && _currentSubtitle!.isNotEmpty) + Positioned( + bottom: 50, + left: 0, + right: 0, + child: Text( + _currentSubtitle!, + style: TextStyle( + fontSize: 19, + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ); + } +} diff --git a/flutter-exoplayer/flutter_beta_exoplayer_creator/pubspec.lock b/flutter-exoplayer/flutter_beta_exoplayer_creator/pubspec.lock index ebf4939..991cdd4 100644 --- a/flutter-exoplayer/flutter_beta_exoplayer_creator/pubspec.lock +++ b/flutter-exoplayer/flutter_beta_exoplayer_creator/pubspec.lock @@ -129,6 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030 + url: "https://pub.dev" + source: hosted + version: "5.5.0" flutter: dependency: "direct main" description: flutter @@ -150,6 +158,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" + url: "https://pub.dev" + source: hosted + version: "2.0.24" flutter_test: dependency: "direct dev" description: flutter @@ -304,6 +320,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5 + url: "https://pub.dev" + source: hosted + version: "10.4.5" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47" + url: "https://pub.dev" + source: hosted + version: "10.3.6" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" + url: "https://pub.dev" + source: hosted + version: "9.1.4" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4" + url: "https://pub.dev" + source: hosted + version: "3.12.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 + url: "https://pub.dev" + source: hosted + version: "0.1.3" petitparser: dependency: transitive description: diff --git a/flutter-exoplayer/flutter_beta_exoplayer_creator/pubspec.yaml b/flutter-exoplayer/flutter_beta_exoplayer_creator/pubspec.yaml index 78f231f..5709eb1 100644 --- a/flutter-exoplayer/flutter_beta_exoplayer_creator/pubspec.yaml +++ b/flutter-exoplayer/flutter_beta_exoplayer_creator/pubspec.yaml @@ -33,6 +33,8 @@ dependencies: video_player: ^2.3.0 # Make sure to check for the latest version chewie: ^1.2.2 # Optional, if you want to use a higher-level video player with controls keep_screen_on: ^3.0.0 # Add this line + file_picker: ^5.2.2 # Use the latest version + permission_handler: ^10.2.0 dev_dependencies: flutter_test: @@ -51,7 +53,7 @@ dev_dependencies: flutter_icons: android: true ios: true - image_path: "assets/icon/icon.png" + image_path: "assets/icon.png" # The following section is specific to Flutter packages. flutter: