카테고리 보관물: code

플러터 자막 지원 비디오 플레이어

저번 달에 Flutter를 사용하여 SRT 자막을 지원하는 간단한 비디오 플레이어를 만들어 보았습니다

하루 만에 만들 수 있을 것 같아서 작성을 시작했는데 (늘 그렇듯이) 예상 시간 보다 2배 정도 걸렸습니다.

스샷의 영상은 ‘매니지먼트 숲 MANAGEMENT SOOP Official’채널의 ‘(ENG)민주의 음악중심 마지막 날 VLOG | Minju Vlog’ 영상이며, 영어 자막은 Whisper를 사용하여 만들었습니다

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"))));
}
}

그림을 조각내어 섞는 코드

그림을 조각내어서 퍼즐처럼 섞어주는 프로그램, pillow 라이브러리가 필요하다.

puzzled_image.py sample.jpg 4 4 2
처럼 사용하면 된다.

# puzzled_image.py
# pip install pillow
import os
import sys
import random
from PIL import Image
from PIL import ImageOps
def puzzled_image(filepath, count_x, count_y,shuffle_count, invert=False):
filename = os.path.splitext(filepath)[0]
try:
srcim = Image.open(filepath)
cropX = int(srcim.size[0]/count_x)
cropY = int(srcim.size[1]/count_y)
srcBoxs = []
dstBoxs = []
for i in range(0, count_y):
for j in range(0, count_x):
x = j * cropX
y = i * cropY
bb = (x, y, x+cropX , y+cropY)
srcBoxs.append(bb)
dstBoxs.append(bb)
for i in range(shuffle_count):
random.shuffle(srcBoxs)
dstim = Image.new('RGB', srcim.size)
count = len(srcBoxs)
for i in range(0, count):
p = srcim.crop(srcBoxs[i])
if invert:
if i%2 == 0:
p = ImageOps.invert(p)
dstim.paste(p, dstBoxs[i])
# print(srcBoxs[i])
dstim.save(filename + "_puzzled.png", "PNG")
except e:
print(e)
return
if __name__ == '__main__':
argc = len(sys.argv)
if argc != 5:
print("Usage:\tpuzzled_image.py <image_file_path x y shuffle_count>\n\tx,y is number of pieces.")
else:
countX = int(sys.argv[2])
countY = int(sys.argv[3])
shuffle_count = int(sys.argv[4])
puzzled_image(sys.argv[1], countX, countY, shuffle_count, False)

python에서 paramiko 라이브러리를 사용하여 ssh 접속 및 명령 실행하기


서버를 관리할 일이 있었는데 Ansible을 쓰는 것이 좋은 방법이지만, 나는 간단한 기능을 원했고 마음대로 커스터마이즈 하고 싶어서 python으로 코드를 만들었다.

import paramiko
import re
import time
# reference sites
# * https://stackoverflow.com/questions/1911690/nested-ssh-session-with-paramiko
# * https://stackoverflow.com/questions/53707630/paramiko-how-to-detect-if-command-executed-using-invoke-shell-has-finished
# use paramiko library
class SSHConnect:
def __init__(self):
self.client = paramiko.SSHClient()
self.transport = None
def connectAndOpen(self, host, port, username, password):
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy)
self.client.connect( hostname = host, port = port, username = username, password = password, allow_agent = True )
print ("Connected to %s" % host)
self.transport = self.client.get_transport()
self.password = password
return
def runCommand(self, commandText):
print("> %s" % commandText)
self.session = self.transport.open_session()
self.session.set_combine_stderr(True)
self.session.get_pty()
self.session.setblocking(1)
self.session.exec_command(commandText)
loop = 1
while loop > 0:
loop = loop – 1
while self.session.exit_status_ready()==False:
stdout = self.session.recv(1024).decode("utf-8")
# When requesting password input, put the stored password.
if re.search('[Pp]assword', stdout):
self.session.send(self.password+'\n')
loop = loop + 1
else:
print(stdout, end='')
self.session.close()
return
def close(self):
self.transport.close()
self.transport = None
self.client.close()
self.client = None
Host = '127.0.0.1'
Port = 22
ID = ''
Password = ''
con = SSHConnect()
con.connectAndOpen(Host, Port, ID, Password)
print("Start")
con.runCommand("df -h")
time.sleep(1)
con.runCommand("sudo apt update && sudo apt upgrade -y")
print("End")
con.close()

위 코드는 패스워드를 사용해서 접속하고 sudo 등의 커맨드에서 password 를 자동으로 입력해주는 샘플 코드다. 실제로 쓰는 코드는 ssh키도 사용할 수 있고, 서버 설정 파일을 cryptography 라이브러리를 사용하여 암호화 시켜서 쓴다.

그리고,실행 중에 화면 입력을 필요로 할 때에는 무한 대기 상태에 빠져버리는 문제가 있다.