Added Flutter v5 source code
@ -1,55 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:media3_exoplayer_creator/screens/video_screen.dart'; // Import other files as needed
|
||||
|
||||
void main() {
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
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(
|
||||
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,
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.blueAccent, // Set text color to blue for TextButtons
|
||||
),
|
||||
),
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,176 +0,0 @@
|
||||
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<VideoScreen> {
|
||||
String _videoUrl = '';
|
||||
String _filePath = '';
|
||||
String _subtitleUrl = ''; // Subtitle URL variable
|
||||
String _subtitleFilePath = ''; // Subtitle file path
|
||||
|
||||
// Method to show video URL dialog
|
||||
Future<void> _showVideoURLDialog() async {
|
||||
final TextEditingController videoController = TextEditingController();
|
||||
|
||||
return showDialog<void>(
|
||||
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: <Widget>[
|
||||
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<void> _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<void> _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<void> _enterSubtitleURL() async {
|
||||
final TextEditingController subtitleController = TextEditingController();
|
||||
|
||||
return showDialog<void>(
|
||||
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: <Widget>[
|
||||
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
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
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<VideoPlayerWidget> {
|
||||
late VideoPlayerController _videoPlayerController;
|
||||
late ChewieController _chewieController;
|
||||
late List<WebVttCue> _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<void> _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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 544 B |
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 442 B |
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 721 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
109
flutter-exoplayer/media3_exoplayer_creator/lib/main.dart
Normal file
@ -0,0 +1,109 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:media3_exoplayer_creator/screens/video_screen.dart'; // Import other files as needed
|
||||
|
||||
void main() {
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
bool _isDarkMode = false;
|
||||
|
||||
void _toggleTheme() {
|
||||
setState(() {
|
||||
_isDarkMode = !_isDarkMode;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: _isDarkMode
|
||||
? ThemeData.dark().copyWith(
|
||||
primaryColor: Colors.blue,
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
primarySwatch: Colors.lightBlue)
|
||||
.copyWith(secondary: Colors.blue),
|
||||
appBarTheme: const AppBarTheme(
|
||||
color: Colors.blue,
|
||||
titleTextStyle: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.blue,
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blue,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blue,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: ThemeData.light().copyWith(
|
||||
primaryColor: Colors.blue,
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
primarySwatch: Colors.lightBlue)
|
||||
.copyWith(secondary: Colors.blue),
|
||||
appBarTheme: const AppBarTheme(
|
||||
color: Colors.blue,
|
||||
titleTextStyle: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.blue,
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blue,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blue,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
home: Scaffold(
|
||||
body: VideoScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,277 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import '../widgets/video_player_widget.dart';
|
||||
import 'package:media3_exoplayer_creator/main.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<VideoScreen> {
|
||||
String _videoUrl = '';
|
||||
String _filePath = '';
|
||||
String _subtitleUrl = ''; // Subtitle URL variable
|
||||
String _subtitleFilePath = ''; // Subtitle file path
|
||||
bool _isDarkMode = false; // Add theme state here
|
||||
|
||||
// Method to show video URL dialog
|
||||
Future<void> _showVideoURLDialog() async {
|
||||
final TextEditingController videoController = TextEditingController();
|
||||
|
||||
return showDialog<void>(
|
||||
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: <Widget>[
|
||||
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<void> _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<void> _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<void> _enterSubtitleURL() async {
|
||||
final TextEditingController subtitleController = TextEditingController();
|
||||
|
||||
return showDialog<void>(
|
||||
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: <Widget>[
|
||||
TextButton(
|
||||
child: Text('Cancel'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text('OK'),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_subtitleUrl = subtitleController.text;
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Toggle the theme
|
||||
void _toggleTheme() {
|
||||
setState(() {
|
||||
_isDarkMode = !_isDarkMode;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: _isDarkMode
|
||||
? ThemeData.dark().copyWith(
|
||||
primaryColor: Colors.blue,
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
primarySwatch: Colors.lightBlue)
|
||||
.copyWith(secondary: Colors.blue),
|
||||
appBarTheme: const AppBarTheme(
|
||||
color: Colors.blue,
|
||||
titleTextStyle: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.blue,
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blue,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blue,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: ThemeData.light().copyWith(
|
||||
primaryColor: Colors.blue,
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
primarySwatch: Colors.lightBlue)
|
||||
.copyWith(secondary: Colors.blue),
|
||||
appBarTheme: const AppBarTheme(
|
||||
color: Colors.blue,
|
||||
titleTextStyle: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.blue,
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blue,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.blue,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
dialogTheme: DialogTheme(
|
||||
backgroundColor: Colors.white, // Light background for light mode
|
||||
titleTextStyle: TextStyle(
|
||||
color: Colors.black, // Black text for titles in light mode
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
contentTextStyle: TextStyle(
|
||||
color: Colors.black, // Black text for content in light mode
|
||||
),
|
||||
),
|
||||
),
|
||||
home: Scaffold(
|
||||
appBar: _videoUrl.isEmpty && _filePath.isEmpty
|
||||
? AppBar(
|
||||
title: const Text('ExoPlayer Creator'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(_isDarkMode ? Icons.light_mode : Icons.dark_mode),
|
||||
onPressed: _toggleTheme,
|
||||
),
|
||||
],
|
||||
)
|
||||
: 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
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -10,13 +10,14 @@ class WebVttCue {
|
||||
});
|
||||
}
|
||||
|
||||
// Subtitle parsing logic (WebVTT format)
|
||||
List<WebVttCue> 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<WebVttCue> cues = [];
|
||||
|
||||
for (final match in cuePattern.allMatches(subtitleData)) {
|
||||
final start = parseTime(match.group(1)!);
|
||||
final end = parseTime(match.group(2)!);
|
||||
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));
|
||||
}
|
||||
@ -24,7 +25,8 @@ List<WebVttCue> parseWebVtt(String subtitleData) {
|
||||
return cues;
|
||||
}
|
||||
|
||||
Duration parseTime(String time) {
|
||||
// Helper method to parse time string to Duration
|
||||
Duration _parseTime(String time) {
|
||||
final parts = time.split(':');
|
||||
final secondsParts = parts[2].split('.');
|
||||
return Duration(
|
@ -0,0 +1,199 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
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:keep_screen_on/keep_screen_on.dart'; // Import keep_screen_on
|
||||
import 'package:flutter/services.dart'; // Import SystemChrome
|
||||
import '../utils/web_vtt.dart'; // Import web_vtt.dart for subtitle handling
|
||||
|
||||
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<VideoPlayerWidget> {
|
||||
late VideoPlayerController _videoPlayerController;
|
||||
late ChewieController _chewieController;
|
||||
late List<WebVttCue> _subtitles;
|
||||
String? _currentSubtitle;
|
||||
bool _isLoading = true; // Flag to track loading state of subtitles
|
||||
|
||||
@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();
|
||||
|
||||
// Keep the screen on while the video is playing
|
||||
KeepScreenOn.turnOn();
|
||||
|
||||
// Hide system UI (status bar and navigation bar) when the video starts
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||||
|
||||
// Listen to video position changes to update subtitles
|
||||
_videoPlayerController.addListener(_updateCurrentSubtitle);
|
||||
|
||||
// Initialize the video player
|
||||
_initializeVideoPlayer();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Turn off the screen stay-on feature and reset the system UI when the widget is disposed
|
||||
KeepScreenOn.turnOff();
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); // Restore UI to default
|
||||
|
||||
_videoPlayerController.removeListener(_updateCurrentSubtitle);
|
||||
_videoPlayerController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Method to load subtitles (you can adapt it to your subtitle parsing logic)
|
||||
Future<void> _loadSubtitles() async {
|
||||
setState(() {
|
||||
_isLoading = true; // Start loading subtitles
|
||||
});
|
||||
|
||||
// Check if subtitle path is provided
|
||||
if (widget.subtitleFilePath.isNotEmpty) {
|
||||
// Load subtitle from local file
|
||||
try {
|
||||
final file = File(widget.subtitleFilePath);
|
||||
final subtitleData = await file.readAsString();
|
||||
setState(() {
|
||||
_subtitles = parseWebVtt(subtitleData); // Use parseWebVtt from web_vtt.dart
|
||||
_isLoading = false; // Subtitles loaded successfully
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoading = false; // Failed to load subtitles
|
||||
});
|
||||
print('Error loading subtitle file: $e');
|
||||
}
|
||||
} else if (widget.subtitleUrl.isNotEmpty) {
|
||||
// Load subtitle from URL
|
||||
try {
|
||||
final response = await http.get(Uri.parse(widget.subtitleUrl));
|
||||
if (response.statusCode == 200) {
|
||||
setState(() {
|
||||
_subtitles = parseWebVtt(response.body); // Use parseWebVtt from web_vtt.dart
|
||||
_isLoading = false; // Subtitles loaded successfully
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_isLoading = false; // Failed to load subtitles from URL
|
||||
});
|
||||
print('Failed to load subtitle from URL: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoading = false; // Failed to load subtitles
|
||||
});
|
||||
print('Error loading subtitle from URL: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method to update the current subtitle based on the video position
|
||||
void _updateCurrentSubtitle() {
|
||||
final currentTime = _videoPlayerController.value.position;
|
||||
|
||||
for (var cue in _subtitles) {
|
||||
if (currentTime >= cue.start && currentTime <= cue.end) {
|
||||
setState(() {
|
||||
_currentSubtitle = cue.text;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_currentSubtitle = '';
|
||||
});
|
||||
}
|
||||
|
||||
void _initializeVideoPlayer() async {
|
||||
// Initialize the video player
|
||||
await _videoPlayerController.initialize();
|
||||
|
||||
// Update loading state once the video is ready
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
// Background set to black
|
||||
Container(
|
||||
color: Colors.black,
|
||||
child: Chewie(controller: _chewieController), // Video player widget
|
||||
),
|
||||
// Show a loading indicator while video or subtitles are loading
|
||||
if (_isLoading)
|
||||
Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
// Display the current subtitle if available
|
||||
if (_currentSubtitle != null && _currentSubtitle!.isNotEmpty && !_isLoading)
|
||||
Positioned(
|
||||
bottom: 70, // Adjusted the bottom padding to be higher
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
color: Colors.black.withOpacity(0.7), // Black background with transparency
|
||||
child: Text(
|
||||
_currentSubtitle!,
|
||||
style: TextStyle(
|
||||
fontSize: 19,
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -81,6 +81,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.0"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.4+2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -133,10 +141,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030
|
||||
sha256: c904b4ab56d53385563c7c39d8e9fa9af086f91495dfc48717ad84a42c3cf204
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.5.0"
|
||||
version: "8.1.7"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -324,42 +332,50 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: permission_handler
|
||||
sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5
|
||||
sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.4.5"
|
||||
version: "11.3.1"
|
||||
permission_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47"
|
||||
sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.3.6"
|
||||
version: "12.0.13"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5"
|
||||
sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.1.4"
|
||||
version: "9.4.5"
|
||||
permission_handler_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_html
|
||||
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3+5"
|
||||
permission_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_platform_interface
|
||||
sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4"
|
||||
sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.12.0"
|
||||
version: "4.2.3"
|
||||
permission_handler_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_windows
|
||||
sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098
|
||||
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
version: "0.2.1"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -453,6 +469,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.1"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.14"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.3"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
@ -30,11 +30,12 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
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
|
||||
video_player: ^2.9.2 # Make sure to check for the latest version
|
||||
chewie: ^1.8.5 # 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
|
||||
file_picker: ^8.1.7 # Use the latest version
|
||||
permission_handler: ^11.3.1
|
||||
url_launcher: ^6.3.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@ -50,7 +51,7 @@ dev_dependencies:
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
flutter_icons:
|
||||
flutter_launcher_icons:
|
||||
android: true
|
||||
ios: true
|
||||
image_path: "assets/icon.png"
|
||||
@ -59,14 +60,14 @@ flutter_icons:
|
||||
flutter:
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# included with your application, so that you can use the icons ina
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
assets:
|
||||
- assets/ani.png
|
||||
- assets/icon.png
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/to/resolution-aware-images
|