|
import 'dart:io'; |
|
import 'package:flutter/material.dart'; |
|
import 'package:path/path.dart' as Path; |
|
|
|
import 'package:desktop_drop/desktop_drop.dart'; |
|
import 'package:video_player_win/video_player_win.dart'; |
|
|
|
// Simple video player with subtitle support (Flutter) |
|
|
|
/* Using package |
|
intl, desktop_drop, video_player_win |
|
*/ |
|
|
|
void main() { |
|
runApp(const MyApp()); |
|
} |
|
|
|
class Subtitle { |
|
final int index; |
|
final int startTime; |
|
final int endTime; |
|
final List<String> text; |
|
|
|
Subtitle(this.index, this.startTime, this.endTime, this.text); |
|
} |
|
|
|
class SrtSubTitle { |
|
List<Subtitle> subtitles = []; |
|
|
|
int parseSrtTimeToSec(String srtTime) { |
|
final timeParts = srtTime.split(':'); |
|
final hour = int.parse(timeParts[0]); |
|
final minute = int.parse(timeParts[1]); |
|
final secondParts = timeParts[2].split(','); |
|
final second = int.parse(secondParts[0]); |
|
final millisecond = int.parse(secondParts[1]) > 500 ? 1 : 0; |
|
return second + minute * 60 + hour * 3600 + millisecond; |
|
} |
|
|
|
void parseSrtFile(String filePath) { |
|
subtitles.clear(); |
|
|
|
final file = File(filePath); |
|
if (file.existsSync() == false) { |
|
return; |
|
} |
|
|
|
final lines = file.readAsLinesSync(); |
|
int currentIndex = 0; |
|
|
|
for (int i = 0; i < lines.length; i++) { |
|
final line = lines[i].trim(); |
|
|
|
if (line.isEmpty) { |
|
continue; |
|
} |
|
|
|
if (int.tryParse(line) != null) { |
|
currentIndex = int.parse(line); |
|
} else if (line.contains('–>')) { |
|
final timeSplit = line.split('–>'); |
|
final startTime = parseSrtTimeToSec(timeSplit[0].trim()); |
|
final endTime = parseSrtTimeToSec(timeSplit[1].trim()); |
|
|
|
final textLines = <String>[]; |
|
i++; |
|
|
|
while (i < lines.length && lines[i].trim().isNotEmpty) { |
|
textLines.add(lines[i].trim()); |
|
i++; |
|
} |
|
final subtitle = Subtitle(currentIndex, startTime, endTime, textLines); |
|
subtitles.add(subtitle); |
|
} |
|
} |
|
|
|
return; |
|
} |
|
|
|
bool _timedSubtitle(Subtitle subtitle, int timeStampSec) { |
|
return (subtitle.startTime <= timeStampSec && |
|
timeStampSec < subtitle.endTime); |
|
} |
|
|
|
Subtitle? getCurrentSubtitle(int timeStampSec) { |
|
if (subtitles.isEmpty) { |
|
return null; |
|
} |
|
|
|
int subtitleLength = subtitles.length; |
|
|
|
for (int i = 0; i < subtitleLength; i++) { |
|
final subtitle = subtitles[i]; |
|
if (_timedSubtitle(subtitle, timeStampSec)) { |
|
return subtitle; |
|
} |
|
} |
|
|
|
return null; |
|
} |
|
} |
|
|
|
class MyApp extends StatelessWidget { |
|
const MyApp({super.key}); |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
const title = 'Simple video player with subtitle support'; |
|
return MaterialApp( |
|
title: title, |
|
theme: ThemeData( |
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blueAccent), |
|
useMaterial3: true, |
|
), |
|
home: const MyHomePage(title: title), |
|
); |
|
} |
|
} |
|
|
|
class MyHomePage extends StatefulWidget { |
|
const MyHomePage({super.key, required this.title}); |
|
|
|
final String title; |
|
|
|
@override |
|
State<MyHomePage> createState() => _MyHomePageState(); |
|
} |
|
|
|
class _MyHomePageState extends State<MyHomePage> { |
|
WinVideoPlayerController? controller; |
|
SrtSubTitle srtSubTitle = SrtSubTitle(); |
|
|
|
@override |
|
void initState() { |
|
super.initState(); |
|
} |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
Widget? playerWidget; |
|
|
|
if (controller != null) { |
|
Widget overlayWidget = Container( |
|
color: Colors.white.withOpacity(0.5), |
|
child: ValueListenableBuilder( |
|
valueListenable: controller!, |
|
builder: (context, value, child) { |
|
final minute = controller!.value.position.inMinutes; |
|
final second = controller!.value.position.inSeconds % 60; |
|
final timeStampSec = second + minute * 60; |
|
|
|
final subtitle = srtSubTitle.getCurrentSubtitle(timeStampSec); |
|
return Column(children: [ |
|
subtitle == null |
|
? const Text('') |
|
: Text(subtitle.text.join('\n')), |
|
Row(mainAxisAlignment: MainAxisAlignment.center, children: [ |
|
Text("[ $minute:$second ]"), |
|
TextButton( |
|
onPressed: () => controller!.play(), |
|
child: const Text("Play")), |
|
TextButton( |
|
onPressed: () => controller!.pause(), |
|
child: const Text("Pause")), |
|
TextButton( |
|
onPressed: () => controller!.seekTo(Duration( |
|
milliseconds: |
|
controller!.value.position.inMilliseconds + |
|
10 * 1000)), |
|
child: const Text("Forward")), |
|
]) |
|
]); |
|
})); |
|
|
|
playerWidget = Stack(children: [ |
|
WinVideoPlayer(controller!), |
|
Positioned(bottom: 0, left: 0, right: 0, child: overlayWidget) |
|
]); |
|
} |
|
|
|
return Scaffold( |
|
body: DropTarget( |
|
onDragDone: (details) { |
|
var path = details.files[0].path; |
|
var c = WinVideoPlayerController.file(File(path)); |
|
if (controller != null) { |
|
controller!.dispose(); |
|
controller = null; |
|
} |
|
|
|
c.initialize().then((value) { |
|
if (c.value.isInitialized) { |
|
var srtFilename = "${Path.withoutExtension(path)}.srt"; |
|
srtSubTitle.parseSrtFile(srtFilename); |
|
|
|
c.play(); |
|
setState(() { |
|
controller = c; |
|
}); |
|
} else { |
|
debugPrint("video file load failed"); |
|
} |
|
}); |
|
}, |
|
child: playerWidget ?? |
|
const Center(child: Text("Drop video file here")))); |
|
} |
|
} |