AV1 Video Converter
- Bu yazımda, daha önce hakkında bilgi verdiğim AV1 Codec için dönüştürücü programının nasıl yazılacağı ile ilgili bilgiler paylaşacağım.
- AV1 video dönüştürücü uygulamamızın oluşturulmasına geçmeden önce, kullanacağımız Wails framework’ü hakkında biraz bilgi sahibi olalım.
Wails Nedir?
Wails, Go ve web teknolojilerini kullanarak masaüstü uygulamaları oluşturmanıza olanak tanıyan açık kaynaklı bir projedir. Kavram olarak Electron’a benzer, ancak bazı önemli farklılıkları vardır:
-
Go Backend: Electron’un Node.js kullanmasının aksine, Wails backend için Go’yu kullanır. Bu, geliştiricilerin Go’nun performansından ve geniş standart kütüphanesinden yararlanmasına olanak tanır.
-
Daha Küçük Boyut: Wails uygulamaları genellikle Electron uygulamalarına göre daha küçük dosya boyutuna sahiptir ve daha az bellek kullanır, çünkü tam bir Chromium tarayıcısı içermezler.
-
Yerel Render: Wails, işletim sisteminin yerel webview’ını kullanır, bu da daha iyi performans ve daha yerel bir his sağlar.
-
Çoklu Platform Desteği: Electron gibi, Wails de Windows, macOS ve Linux için uygulama oluşturmayı destekler.
-
Frontend Esnekliği: Örneğimiz Svelte kullanırken, Wails React, Vue veya saf JavaScript gibi çeşitli frontend framework’leriyle uyumludur.
Wails, web teknolojileri ile Go arasında köprü kurarak, geliştiricilerin modern ve duyarlı bir kullanıcı arayüzüne sahip güçlü masaüstü uygulamaları oluşturmasına olanak tanır. Özellikle Go’da rahat olan ve platforma özel GUI framework’lerine dalmadan masaüstü uygulamaları oluşturmak isteyen geliştiriciler için uygundur.
Artık Wails’in ne olduğunu anladığımıza göre, bu güçlü framework’ü kullanarak AV1 Video Dönüştürücü uygulamamızı oluşturmaya geçelim.
AV1 Video Dönüştürücü Uygulaması Yazmaya Başlayalım
Uygulamayı yazmaya başlamadan önce nelere ihtiyacımız olacak bir göz atalım, aşağıdaki gereksinimlerin bilgisayarınız da olduğunu düşünerek bunların kurulumuna girmeyeceğim ama bunlar için detaylı bilgi isterseniz bana ulaşabilirsiniz.
- Go programlama dili
- Node.js ve npm
- Wails framework
- Svelte
- FFmpeg ve FFprobe
- Uygulamamızın derlenmiş görünümü aşağıdaki gibi oalcak
*Şekil 1: Uygulamanın görüntüsü *
Adım 1: Proje Yapısını Oluşturma
- GO Lang’in bilgisayarınızda kurulu olduğunu varsayarak Wails kurulumu ile başlıyoruz. Eğer hala Go programlama dilini kurmadıysanız, Go Resmi Sayfası kurulum talimatlarını takip ederek kurabilirsiniz.
- Komut satırını açıyoruz ve aşağıdaki komut ile wails framework’üni kuruyoruz.
1
|
go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
- Wails projemizi oluşturalım
1
|
wails init -n AV1-video-converter -t svelte
|
- Proje dizinine geçelim
- Artık projemiz bu dizinde oluşmuş bir halde, şimdi kodlarımızı yazmaya başlayalım
Adım 2: Go kodlarını yazma
Ben proje dosyaları içerisinde olan 3 adet dosyanın içini kod yazacağım ugulamamızın ana dosyları “app.go” , “main.go” ve “app.svelte” den oluşacak
Bölüm 1: Backend (app.go)
1.1 Paket ve Import tanımları
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package main
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
|
- Bu bölümde, uygulamanın ihtiyaç duyduğu Go paketleri import edilir. Önemli paketler arasında os, exec, filepath, ve Wails runtime bulunur.
1.2 Veri Yapıları
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// VideoInfo struct
// Represents information about a video file
// Bir video dosyası hakkında bilgileri temsil eder
type VideoInfo struct {
FullPath string `json:"fullPath"` // Full path of the video file / Video dosyasının tam yolu
Duration string `json:"duration"` // Duration of the video / Videonun süresi
FrameCount int `json:"frameCount"` // Total number of frames / Toplam kare sayısı
Codec string `json:"codec"` // Video codec / Video kodeki
Size string `json:"size"` // File size / Dosya boyutu
}
// App struct
// Represents the main application structure
// Ana uygulama yapısını temsil eder
type App struct {
ctx context.Context // Application context / Uygulama bağlamı
appDir string // Application directory / Uygulama dizini
ffmpegPath string // Path to FFmpeg executable / FFmpeg yürütülebilir dosyasının yolu
ffprobePath string // Path to FFprobe executable / FFprobe yürütülebilir dosyasının yolu
logFile *os.File // Log file / Log dosyası
configPath string // Path to config file / Yapılandırma dosyasının yolu
lastDestination string // Last used destination folder / Son kullanılan hedef klasör
}
|
- VideoInfo struct’ı, bir video dosyasının özelliklerini temsil eder. App struct’ı ise uygulamanın genel durumunu ve yapılandırmasını içerir.
1.3 Uygulama Başlatma ve Kapatma
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
|
// NewApp creates a new App application struct
// Creates and returns a new instance of the App struct
// App yapısının yeni bir örneğini oluşturur ve döndürür
func NewApp() *App {
return &App{}
}
// startup is called when the app starts
// Initializes the application, sets up logging, and finds FFmpeg/FFprobe
// Uygulama başladığında çağrılır, Log kaydını ayarlar ve FFmpeg/FFprobe'u bulur
func (a *App) startup(ctx context.Context) {
// Save the context
// Bağlamı kaydet
a.ctx = ctx
// Get the executable path
// Yürütülebilir dosya yolunu al
executablePath, err := os.Executable()
if err != nil {
log.Fatal("Error getting executable path:", err)
}
a.appDir = filepath.Dir(executablePath)
// Special case for MacOS application
// MacOS uygulaması için özel durum
if strings.HasSuffix(a.appDir, "MacOS") {
a.appDir = filepath.Dir(filepath.Dir(a.appDir))
}
// Log current working directory and executable path
// Mevcut çalışma dizinini ve yürütülebilir dosya yolunu günlüğe kaydet
log.Printf("Current working directory: %s", a.appDir)
log.Printf("Executable path: %s", os.Args[0])
// Setup logging
// Log kaydını ayarla
logsDir := filepath.Join(a.appDir, "logs")
if err := os.MkdirAll(logsDir, 0755); err != nil {
log.Fatal("Error creating logs directory:", err)
}
// Perform cleanup operations
// Temizleme işlemlerini gerçekleştir
a.cleanupLogs(logsDir)
// Clear and reopen app.log file
// app.log dosyasını temizle ve yeniden aç
appLogPath := filepath.Join(logsDir, "app.log")
if err := os.Truncate(appLogPath, 0); err != nil {
log.Printf("Error truncating app.log: %v", err)
}
a.logFile, err = os.OpenFile(appLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal("Error opening log file:", err)
}
log.SetOutput(a.logFile)
// Find FFmpeg and FFprobe
// FFmpeg ve FFprobe'u bul
a.ffmpegPath = a.findExecutable("ffmpeg")
a.ffprobePath = a.findExecutable("ffprobe")
if a.ffmpegPath == "" || a.ffprobePath == "" {
log.Fatal("FFmpeg or FFprobe not found. Please ensure both are installed and available in the application bundle or system PATH.")
}
log.Printf("Using FFmpeg: %s", a.ffmpegPath)
log.Printf("Using FFprobe: %s", a.ffprobePath)
// Load config
// Yapılandırmayı yükle
a.configPath = filepath.Join(a.appDir, "config.json")
a.loadConfig()
}
// shutdown is called at application termination
// Performs cleanup operations when the application is closing
// Uygulama kapanırken temizleme işlemlerini gerçekleştirir
func (a *App) shutdown(ctx context.Context) {
// Close the log file if it's open
// Log dosyası açıksa kapat
if a.logFile != nil {
a.logFile.Close()
}
}
|
- startup fonksiyonu, uygulama başlatıldığında çalışır ve gerekli yapılandırmaları yapar. shutdown fonksiyonu ise uygulama kapatılırken temizlik işlemlerini gerçekleştirir.
1.4 FFmpeg ve FFprobe lokasyonlarını bulma
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
// findExecutable locates the specified executable in various paths
// Searches for the given executable in predefined paths and PATH
// Belirtilen yürütülebilir dosyayı çeşitli yollarda arar
func (a *App) findExecutable(name string) string {
// Define possible paths for the executable
// Yürütülebilir dosya için olası yolları tanımla
possiblePaths := []string{
filepath.Join(filepath.Dir(os.Args[0]), name),
filepath.Join(a.appDir, name),
filepath.Join("/usr/local/bin", name),
filepath.Join("/opt/homebrew/bin", name),
}
// Check each possible path
// Her olası yolu kontrol et
for _, path := range possiblePaths {
log.Printf("Checking for %s at: %s", name, path)
if _, err := os.Stat(path); err == nil {
log.Printf("Found %s at: %s", name, path)
return path
}
}
// If not found in the above locations, check in system PATH
// Yukarıdaki konumlarda bulunamazsa, sistem PATH'inde kontrol et
path, err := exec.LookPath(name)
if err == nil {
log.Printf("Found %s in PATH: %s", name, path)
return path
}
log.Printf("%s not found in any expected location or system PATH", name)
return ""
}
|
- FFmpeg ve FFprobe kurulumlarının lokasyonlarını arar,
1.5 Log ve Konfig işlemleri
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
// cleanupLogs removes old log files
// Deletes log files older than 24 hours, except for app.log
// 24 saatten eski Log dosyalarını siler, app.log hariç
func (a *App) cleanupLogs(logsDir string) {
// Read all files in the logs directory
// Log dizinindeki tüm dosyaları oku
files, err := ioutil.ReadDir(logsDir)
if err != nil {
log.Printf("Error reading logs directory: %v", err)
return
}
now := time.Now()
for _, file := range files {
if file.Name() == "app.log" {
continue // Skip app.log / app.log'u atla
}
filePath := filepath.Join(logsDir, file.Name())
if now.Sub(file.ModTime()) > 24*time.Hour {
if err := os.Remove(filePath); err != nil {
log.Printf("Error removing old log file %s: %v", filePath, err)
} else {
log.Printf("Removed old log file: %s", filePath)
}
}
}
}
// loadConfig reads the configuration file
// Loads the last used destination folder from the config file
// Yapılandırma dosyasından son kullanılan hedef klasörü yükler
func (a *App) loadConfig() {
// Read the config file
// Yapılandırma dosyasını oku
data, err := ioutil.ReadFile(a.configPath)
if err != nil {
log.Printf("Error reading config file: %v", err)
return
}
// Unmarshal the JSON data
// JSON verisini çöz
var config struct {
LastDestination string `json:"lastDestination"`
}
if err := json.Unmarshal(data, &config); err != nil {
log.Printf("Error unmarshalling config: %v", err)
return
}
// Set the last destination
// Son hedefi ayarla
a.lastDestination = config.LastDestination
}
// saveConfig writes the current configuration to file
// Saves the current destination folder to the config file
// Mevcut hedef klasörü yapılandırma dosyasına kaydeder
func (a *App) saveConfig() {
// Prepare the config data
// Yapılandırma verisini hazırla
config := struct {
LastDestination string `json:"lastDestination"`
}{
LastDestination: a.lastDestination,
}
// Marshal the config to JSON
// Yapılandırmayı JSON'a dönüştür
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
log.Printf("Error marshalling config: %v", err)
return
}
// Write the config to file
// Yapılandırmayı dosyaya yaz
if err := ioutil.WriteFile(a.configPath, data, 0644); err != nil {
log.Printf("Error writing config file: %v", err)
}
}
|
1.6 Dosya ve Klasör İşlemleri
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
|
// SelectVideoFiles opens a file dialog and returns the selected video files info
// Allows user to select multiple video files and returns their information
// Kullanıcının birden çok video dosyası seçmesine izin verir ve bilgilerini döndürür
func (a *App) SelectVideoFiles() ([]VideoInfo, error) {
// Open file dialog for selecting video files
// Video dosyaları seçmek için dosya iletişim kutusunu aç
files, err := runtime.OpenMultipleFilesDialog(a.ctx, runtime.OpenDialogOptions{
Title: "Select Video Files",
Filters: []runtime.FileFilter{
{DisplayName: "Video Files", Pattern: "*.mp4;*.avi;*.mov;*.mkv"},
},
})
if err != nil {
log.Printf("Error selecting files: %v", err)
return nil, err
}
// Process selected files
// Seçilen dosyaları işle
var videoInfos []VideoInfo
for _, file := range files {
log.Printf("Processing file: %s", file)
if _, err := os.Stat(file); os.IsNotExist(err) {
log.Printf("File does not exist: %s", file)
continue
}
info, err := a.getVideoInfo(file)
if err != nil {
log.Printf("Error getting info for %s: %v", file, err)
continue
}
videoInfos = append(videoInfos, info)
log.Printf("Successfully processed file: %s", file)
}
// Return the video information to the frontend
// Video bilgilerini Frontend'e gönder
return videoInfos, nil
}
// SelectDestinationFolder opens a directory dialog and returns the selected folder
// Allows user to choose a destination folder for converted videos
// Kullanıcının dönüştürülen videolar için bir hedef klasör seçmesine izin verir
func (a *App) SelectDestinationFolder() (string, error) {
// Open directory dialog
// Dizin seçim penceresini aç
folder, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{
Title: "Select Destination Folder",
})
if err != nil {
log.Printf("Error selecting destination folder: %v", err)
return "", err
}
// If no folder selected, use last destination or default
// Eğer klasör seçilmediyse, son hedefi veya varsayılanı kullan
if folder == "" {
folder = a.lastDestination
if folder == "" {
folder = filepath.Join(os.Getenv("HOME"), "Desktop")
}
}
// Check if the folder is writable
// Klasörün yazılabilir olup olmadığını kontrol et
testFile := filepath.Join(folder, "test_write_permission.tmp")
f, err := os.Create(testFile)
if err != nil {
log.Printf("Selected folder is not writable: %v", err)
return "", fmt.Errorf("selected folder is not writable: %v", err)
}
f.Close()
os.Remove(testFile)
// Save the selected folder as last destination
// Seçilen klasörü son hedef olarak kaydet
a.lastDestination = folder
a.saveConfig()
// Return the selected folder path to the frontend
// Seçilen klasör yolunu Frontend'e gönder
return folder, nil
}
// GetLastDestination returns the last selected destination folder
// Retrieves the last used destination folder from the app's state
// Uygulamanın configinden son kullanılan hedef klasörü alır
func (a *App) GetLastDestination() string {
// Return the last destination to the frontend
// Son hedefi Frontend'e gönder
return a.lastDestination
}
|
- Bu fonksiyonlar, kullanıcının video dosyalarını ve hedef klasörü seçmesini sağlar. GetLastDestination fonksiyonu ise Uygulamanın son kullanılan hedef klasörü kaydedilen config den çağıırır.
1.7 Video Dönüştürme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
// ConvertVideo converts the input video to SVTAV1 format
// Performs the video conversion using FFmpeg and emits progress events
// FFmpeg kullanarak video dönüşümünü gerçekleştirir ve ilerleme olayları yayar
func (a *App) ConvertVideo(inputPath, outputFolder string, totalFrames int) error {
// Prepare output file name
// Çıktı dosya adını hazırla
outputFileName := filepath.Base(inputPath)
outputFileName = strings.TrimSuffix(outputFileName, filepath.Ext(outputFileName))
outputFileName = sanitizeFileName(outputFileName)
outputPath := filepath.Join(outputFolder, outputFileName+"_av1.mp4")
// Create output directory if it doesn't exist
// Çıktı dizini yoksa oluştur
if err := os.MkdirAll(outputFolder, os.ModePerm); err != nil {
log.Printf("Failed to create output directory: %v", err)
return fmt.Errorf("failed to create output directory: %v", err)
}
// Prepare log file for FFmpeg output
// FFmpeg çıktısı için log dosyasını hazırla
logFileName := outputFileName + "_ffmpeg.log"
logFilePath := filepath.Join(a.appDir, "logs", logFileName)
logFile, err := os.Create(logFilePath)
if err != nil {
log.Printf("Failed to create log file: %v", err)
return fmt.Errorf("failed to create log file: %v", err)
}
defer logFile.Close()
// Prepare FFmpeg command
// FFmpeg komutunu hazırla
cmd := exec.Command(a.ffmpegPath,
"-i", inputPath,
"-c:v", "libsvtav1",
"-crf", "30",
"-preset", "6",
"-svtav1-params", "tune=0",
"-c:a", "copy", "-y",
outputPath)
cmd.Stdout = logFile
cmd.Stderr = logFile
// Start FFmpeg process
// FFmpeg işlemini başlat
if err := cmd.Start(); err != nil {
log.Printf("Failed to start FFmpeg: %v", err)
return fmt.Errorf("failed to start FFmpeg: %v", err)
}
// Monitor progress in a separate goroutine
// İlerlemeyi ayrı bir goroutine'de izle
done := make(chan bool)
go func() {
a.monitorProgress(logFilePath, totalFrames, done)
}()
// Wait for FFmpeg to finish
// FFmpeg'in bitmesini bekle
if err := cmd.Wait(); err != nil {
close(done)
log.Printf("FFmpeg error: %v", err)
runtime.EventsEmit(a.ctx, "conversion:error", err.Error())
return fmt.Errorf("FFmpeg error: %v", err)
}
close(done)
time.Sleep(time.Second) // Short wait for progress bar to reach 100% / İlerleme çubuğunun %100'e ulaşması için kısa bir bekleme
runtime.EventsEmit(a.ctx, "conversion:complete", outputPath)
log.Printf("Conversion completed: %s", outputPath)
// Emit event to process next video
// Sıradaki videoyu işlemek için olay yayınla
runtime.EventsEmit(a.ctx, "conversion:next")
return nil
}
// sanitizeFileName removes or replaces invalid characters in a filename
// Ensures the output filename is valid for the file system
// Çıktı dosya adının dosya sistemi için geçerli olmasını sağlar
func sanitizeFileName(fileName string) string {
// Remove or replace invalid characters
// Geçersiz karakterleri kaldır veya değiştir
fileName = strings.Map(func(r rune) rune {
if r == '/' || r == '\\' || r == ':' || r == '*' || r == '?' || r == '"' || r == '<' || r == '>' || r == '|' {
return '_'
}
return r
}, fileName)
// Truncate filename if it's too long
// Dosya adı çok uzunsa kısalt
if len(fileName) > 200 {
fileName = fileName[:200]
}
return fileName
}
|
- ConvertVideo fonksiyonu, seçilen video dosyasını AV1 formatına dönüştürür.
- “cmd” değişkeninin içeriğindeki ffmpeg komutunu değiştirerek bitrate,çözünürlük,hız, codec vb. özellikleri kendi isteğinize göre ayarlayabilrisiniz.
- Ben burada libsvtav1 kullanarak AV1 codec’e çevirdim siz dilerseniz h265,h264,mpeg-2 yapabilirsiniz.
1.8 İlerleme İzleme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
// monitorProgress tracks the conversion progress and emits update events
// Monitors the FFmpeg log file and sends progress updates to the frontend
// FFmpeg Log dosyasını izler ve ilerleme güncellemelerini Frontend'e gönderir
func (a *App) monitorProgress(logPath string, totalFrames int, done chan bool) {
// Open the log file
// Log dosyasını aç
file, err := os.Open(logPath)
if err != nil {
log.Printf("Error opening log file: %v", err)
return
}
defer file.Close()
// Prepare regular expressions for parsing
// Ayrıştırma için düzenli ifadeleri hazırla
frameRegex := regexp.MustCompile(`frame=\s*(\d+)`)
speedRegex := regexp.MustCompile(`speed=(\S+)`)
var lastProgress float64
for {
select {
case <-done:
// Conversion finished, send 100% progress
// Dönüşüm bitti, %100 bilgisini gönder
runtime.EventsEmit(a.ctx, "conversion:progress", map[string]interface{}{
"progress": 100,
"speed": "",
})
return
default:
// Read the last 1024 bytes of the log file
// Log dosyasının son 1024 baytını oku
file.Seek(-1024, 2)
scanner := bufio.NewScanner(file)
var lastLine string
for scanner.Scan() {
lastLine = scanner.Text()
}
if err := scanner.Err(); err != nil {
log.Printf("Error scanning file: %v", err)
continue
}
// Parse progress information
// İlerleme bilgisini ayrıştır
if strings.Contains(lastLine, "frame=") {
frameMatch := frameRegex.FindStringSubmatch(lastLine)
speedMatch := speedRegex.FindStringSubmatch(lastLine)
if len(frameMatch) > 1 && len(speedMatch) > 1 {
currentFrame, err := strconv.ParseFloat(frameMatch[1], 64)
if err != nil {
log.Printf("Error parsing frame: %v", err)
continue
}
speed := strings.TrimSpace(speedMatch[1])
progress := (currentFrame / float64(totalFrames)) * 100
if progress > 100 {
progress = 100
}
// Send progress update to frontend if progress has increased
// İlerleme artmışsa Frontend'e ilerleme güncellemesi gönder
if progress > lastProgress {
lastProgress = progress
fmt.Printf("İlerleme: %.2f%%, Hız: %s\n", progress, speed)
runtime.EventsEmit(a.ctx, "conversion:progress", map[string]interface{}{
"progress": progress,
"speed": speed,
})
}
}
}
}
time.Sleep(500 * time.Millisecond)
}
}
|
- Bu fonksiyon, dönüştürme işleminin ilerlemesini izler ve frontend’e Svelte tarafına bildirir.
1.9 Main.go Dosyamız
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
package main
import (
"embed"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
//go:embed all:frontend/dist
var assets embed.FS
func main() {
// Create an instance of the app structure
app := NewApp()
// Create application with options
err := wails.Run(&options.App{
Title: "MD-AV1-Converter",
Width: 1024,
Height: 680,
DisableResize: true,
AssetServer: &assetserver.Options{
Assets: assets,
},
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: app.startup,
Bind: []interface{}{
app,
},
})
if err != nil {
println("Error:", err.Error())
}
}
|
- main.go Dosyamız uygulamanın ismini ve çözünürlüklerini buradan değiştirebilirsiniz.
Adım 2: Forntend Svelte kodlarını yazma(app.svelte)
2.1 Script Bölümü
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
|
<script>
// Import necessary Svelte functions and modules
// Gerekli Svelte fonksiyonlarını ve modüllerini içe aktar
import { onMount } from 'svelte';
import { flip } from 'svelte/animate';
import { quintOut } from 'svelte/easing';
import Icon from "./Icon.svelte";
// Initialize state variables for the component
// Bileşen için durum değişkenlerini başlat
let selectedVideos = []; // Array to store selected video information / Seçilen video bilgilerini saklamak için dizi
let progressVideo = null; // Currently processing video / Şu anda işlenen video
let contextMenu = { show: false, x: 0, y: 0, index: -1 }; // Context menu state / Bağlam menüsü durumu
let draggedOverIndex = -1; // Index of the item being dragged over / Üzerine sürüklenen öğenin indeksi
let destinationFolder = ''; // Selected destination folder / Seçilen hedef klasör
let conversionProgress = 0; // Current conversion progress / Mevcut dönüşüm ilerlemesi
let conversionSpeed = ''; // Current conversion speed / Mevcut dönüşüm hızı
let errorMessage = ''; // Error message to display / Görüntülenecek hata mesajı
let showErrorPopup = false; // Whether to show the error popup / Hata Pop'u gösterilip gösterilmeyeceği
// Define table headers with tooltips
// Araç ipuçları ile tablo başlıklarını tanımla
const tableHeaders = [
{ label: "#", tooltip: "Index" },
{ label: "File Path", tooltip: "Full path of the video file" },
{ label: "Duration", tooltip: "Video duration (HH:MM:SS:FF)" },
{ label: "Frames", tooltip: "Total number of frames" },
{ label: "Codec", tooltip: "Video codec" },
{ label: "Size", tooltip: "File size" }
];
// Set up event listeners and initialize data when the component mounts
// Bileşen monte edildiğinde olay dinleyicilerini ayarla ve verileri başlat
onMount(async () => {
// Listen for Wails runtime loaded event
// Wails çalışma zamanı yüklendi olayını dinle
window.runtime.EventsOn("wails:loaded", () => {
console.log("Wails runtime Loaded");
});
// Add click event listener to close context menu
// Bağlam menüsünü kapatmak için tıklama olay dinleyicisi ekle
document.addEventListener('click', closeContextMenu);
// Listen for conversion progress updates from Go backend
// Go Bakcend'den dönüşüm ilerleme güncellemelerini dinle
window.runtime.EventsOn("conversion:progress", (data) => {
console.log("Progress update:", data);
conversionProgress = data.progress;
conversionSpeed = data.speed;
});
// Listen for conversion completion event from Go backend
// Go Bakcend'den dönüşüm tamamlanma olayını dinle
window.runtime.EventsOn("conversion:complete", (outputPath) => {
console.log("Conversion completed:", outputPath);
progressVideo = null;
updateProgressVideo();
});
// Listen for conversion error event from Go backend
// Go Bakcend'den dönüşüm hata olayını dinle
window.runtime.EventsOn("conversion:error", (error) => {
console.error("Conversion error:", error);
errorMessage = error;
showErrorPopup = true;
progressVideo = null;
updateProgressVideo();
});
// Listen for next video conversion event from Go backend
// Go Bakcend'den sonraki video dönüşüm olayını dinle
window.runtime.EventsOn("conversion:next", () => {
updateProgressVideo();
});
// Get the last destination folder from Go backend
// Go Bakcend'den son hedef klasörü al
destinationFolder = await window.go.main.App.GetLastDestination();
});
// Function to handle selecting video files
// Video dosyalarını seçme işlemini yöneten fonksiyon
async function handleSelectFiles() {
try {
// Call Go backend to open file dialog and get video info
// Dosya iletişim kutusunu açmak ve video bilgilerini almak için Go Bakcend'i çağır
const videoInfos = await window.go.main.App.SelectVideoFiles();
if (videoInfos && videoInfos.length > 0) {
selectedVideos = [...selectedVideos, ...videoInfos];
updateProgressVideo();
}
} catch (err) {
console.error("Selected File Error:", err);
showError("Selected File Error: " + err.message);
}
}
// Function to handle selecting destination folder
// Hedef klasör seçme işlemini yöneten fonksiyon
async function handleSelectDestination() {
try {
// Call Go backend to open folder dialog
// Klasör iletişim kutusunu açmak için Go Bakcend'i çağır
const folder = await window.go.main.App.SelectDestinationFolder();
if (folder) {
destinationFolder = folder;
}
} catch (err) {
console.error("Destination folder selection error:", err);
showError("Destination folder selection error: " + err.message);
}
}
// Function to update the current video being processed
// İşlenen mevcut videoyu güncelleyen fonksiyon
function updateProgressVideo() {
if (!progressVideo && selectedVideos.length > 0) {
progressVideo = selectedVideos.shift();
selectedVideos = [...selectedVideos];
startConversion();
}
}
// Function to start the video conversion process
// Video dönüşüm sürecini başlatan fonksiyon
async function startConversion() {
if (progressVideo && destinationFolder) {
conversionProgress = 0;
conversionSpeed = '';
try {
// Call Go backend to start video conversion
// Video dönüşümünü başlatmak için Go Bakcend'i çağır
await window.go.main.App.ConvertVideo(progressVideo.fullPath, destinationFolder, progressVideo.frameCount);
} catch (err) {
console.error("Conversion Error:", err);
showError("Conversion Error: " + err.message);
progressVideo = null;
updateProgressVideo();
}
}
}
// Function to handle drag start event
// Sürükleme başlangıç olayını yöneten fonksiyon
function dragStart(event, index) {
event.dataTransfer.setData('text/plain', index);
event.target.classList.add('dragging');
}
// Function to handle drag end event
// Sürükleme bitiş olayını yöneten fonksiyon
function dragEnd(event) {
event.target.classList.remove('dragging');
draggedOverIndex = -1;
}
// Function to handle drag over event
// Sürükleme üzerinde olayını yöneten fonksiyon
function dragOver(event, index) {
event.preventDefault();
draggedOverIndex = index;
}
// Function to handle drag leave event
// Sürüklemeden ayrılma olayını yöneten fonksiyon
function dragLeave() {
draggedOverIndex = -1;
}
// Function to handle drop event
// Bırakma olayını yöneten fonksiyon
function drop(event, targetIndex) {
event.preventDefault();
const sourceIndex = parseInt(event.dataTransfer.getData('text/plain'));
if (sourceIndex !== targetIndex) {
const items = Array.from(selectedVideos);
const [reorderedItem] = items.splice(sourceIndex, 1);
items.splice(targetIndex, 0, reorderedItem);
selectedVideos = items;
}
draggedOverIndex = -1;
}
// Function to show context menu
// Bağlam menüsünü gösteren fonksiyon
function showContextMenu(event, index) {
event.preventDefault();
contextMenu = {
show: true,
x: event.clientX,
y: event.clientY,
index: index
};
}
// Function to close context menu
// Bağlam menüsünü kapatan fonksiyon
function closeContextMenu() {
contextMenu.show = false;
}
// Function to delete an item from the video list
// Video listesinden bir öğeyi silen fonksiyon
function deleteItem() {
if (contextMenu.index > -1) {
selectedVideos = selectedVideos.filter((_, index) => index !== contextMenu.index);
closeContextMenu();
}
}
// Function to show error message
// Hata mesajını gösteren fonksiyon
function showError(message) {
errorMessage = message;
showErrorPopup = true;
}
// Function to close error popup
// Hata açılır penceresini kapatan fonksiyon
function closeErrorPopup() {
showErrorPopup = false;
errorMessage = '';
}
</script>
|
-
script bölümünde aşağıdaki işlemleri yapıyoruz ;
-
import bildirimleri ve değişken tanımlamaları
-
yaşam döngüsü fonksiyonları
-
dosya ve klasör seçme fonksiyonları
-
dönüştürme ve ilerleme izleme fonksiyonları
-
kullanıcı etkileşimi fonksiyonları
2.2 HTML Bölümü
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
|
<main on:contextmenu|preventDefault>
<h1>AV1 Video Converter</h1>
<!-- Destination folder selector -->
<!-- Hedef klasör seçici -->
<div class="destination-selector">
<button on:click={handleSelectDestination}>Select Destination</button>
<input type="text" bind:value={destinationFolder} readonly placeholder="No destination selected">
</div>
<!-- Progress display for current video conversion -->
<!-- Mevcut video dönüşümü için ilerleme göstergesi -->
<div class="progress-container">
<h2>Progress</h2>
{#if progressVideo}
<table>
<thead>
<tr>
{#each tableHeaders as header}
<th title={header.tooltip}>{header.label}</th>
{/each}
</tr>
</thead>
<tbody>
<tr>
<td>-</td>
<td>{progressVideo.fullPath}</td>
<td>{progressVideo.duration}</td>
<td>{progressVideo.frameCount}</td>
<td>{progressVideo.codec}</td>
<td>{progressVideo.size}</td>
</tr>
</tbody>
</table>
<div class="conversion-progress">
<progress value={conversionProgress} max="100"></progress>
<span>{conversionProgress.toFixed(2)}%</span>
</div>
<div class="conversion-speed">
<span>Speed: {conversionSpeed}</span>
</div>
{:else}
<p>No video in progress</p>
{/if}
</div>
<!-- Button to add new videos -->
<!-- Yeni videolar eklemek için düğme -->
<button class="add-video-btn" on:click={handleSelectFiles}>
<i class="fas fa-plus"></i>
<i class="fas fa-video"></i>
Add Video(s)
</button>
<!-- Table displaying selected videos -->
<!-- Seçilen videoları gösteren tablo -->
<div class="table-container">
<table>
<thead>
<tr>
{#each tableHeaders as header}
<th title={header.tooltip}>{header.label}</th>
{/each}
</tr>
</thead>
<tbody>
{#each selectedVideos as video, index (video.fullPath)}
<tr
animate:flip={{duration: 300, easing: quintOut}}
draggable={true}
on:dragstart={(event) => dragStart(event, index)}
on:dragend={dragEnd}
on:dragover={(event) => dragOver(event, index)}
on:dragleave={dragLeave}
on:drop={(event) => drop(event, index)}
on:contextmenu={(event) => showContextMenu(event, index)}
class:drag-over={draggedOverIndex === index}
>
<td>{index + 1}</td>
<td>{video.fullPath}</td>
<td>{video.duration}</td>
<td>{video.frameCount}</td>
<td>{video.codec}</td>
<td>{video.size}</td>
</tr>
{/each}
</tbody>
</table>
</div>
<!-- Instructions for users -->
<!-- Kullanıcılar için talimatlar -->
<div class="instructions">
<p>Right-click on a video to remove it from the list | Drag and drop to reorder videos</p>
<p>Duration format: Hours:Minutes:Seconds:Frames (HH:MM:SS:FF)</p>
<p>For customization or advanced features, please contact: <a href="mailto:murat@muratdemirci.com.tr">murat@muratdemirci.com.tr</a></p>
</div>
<!-- Context menu for video list items -->
<!-- Video listesi öğeleri için bağlam menüsü -->
{#if contextMenu.show}
<div class="context-menu" style="top: {contextMenu.y}px; left: {contextMenu.x}px;">
<button on:click={deleteItem}>Delete</button>
</div>
{/if}
<!-- Error popup -->
<!-- Hata açılır penceresi -->
{#if showErrorPopup}
<div class="error-popup">
<div class="error-content">
<h3>Error</h3>
<p>{errorMessage}</p>
<button on:click={closeErrorPopup}>Close</button>
</div>
</div>
{/if}
</main>
|
- Bu bölümde kullanıcı arayüzü ile ilgili html tanımlaları yapılıyor, aşağıdaki ana başlıklar
- Hedef klasör seçici
- İlerleme göstergesi
- Video dosyası ekleme butonu
- Video listesi tablosu
- Bağlam menüsü
- Hata popup’ı
2.3 CSS Bölümü
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
|
<style>
@font-face {
font-family: 'Roboto';
src: url('assets/fonts/Roboto-Regular.woff2') format('woff2'),
url('assets/fonts/Roboto-Regular.woff') format('woff');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('assets/fonts/Roboto-Bold.woff2') format('woff2'),
url('assets/fonts/Roboto-Bold.woff') format('woff');
font-weight: 700;
font-style: normal;
}
:root {
--primary-color: #4a90e2;
--secondary-color: #357abD;
--background-color: #343434;
--text-color: #e0e0e0;
--hover-color: #4a4a4a;
--table-bg-color: #2a2a2a;
--table-border-color: #4a4a4a;
}
main {
font-family: 'Roboto', sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: var(--background-color);
color: var(--text-color);
font-size: 14px;
}
h1, h2 {
color: var(--primary-color);
text-align: center;
font-weight: 500;
}
h1 {
font-size: 24px;
}
h2 {
font-size: 20px;
}
button {
font-family: 'Roboto', sans-serif;
background-color: var(--primary-color);
border: none;
color: white;
padding: 8px 16px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 14px;
margin: 10px 0;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.3s;
font-weight: 500;
}
button:hover {
background-color: var(--secondary-color);
}
.add-video-btn {
padding: 8px 16px;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
margin: 10px auto;
width: auto;
height: auto;
border-radius: 4px;
}
.add-video-btn i {
margin-right: 8px;
font-size: 16px; /* İkon boyutunu artır */
}
.destination-selector {
display: flex;
align-items: center;
margin-bottom: 20px;
gap: 10px;
}
.destination-selector button {
flex-shrink: 0;
}
.destination-selector input {
flex-grow: 1;
padding: 8px;
border-radius: 4px;
border: 1px solid var(--table-border-color);
background-color: var(--table-bg-color);
color: var(--text-color);
font-family: 'Roboto', sans-serif;
font-size: 14px;
}
.progress-container, .table-container {
background-color: var(--table-bg-color);
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
overflow: hidden;
margin-bottom: 20px;
}
.progress-container table, .table-container table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
}
.progress-container th, .progress-container td,
.table-container th, .table-container td {
padding: 8px 12px;
text-align: left;
border-bottom: 1px solid var(--table-border-color);
font-size: 12px;
}
.progress-container p {
text-align: center;
padding: 16px;
color: var(--text-color);
font-size: 14px;
}
.table-container {
flex: 1;
overflow-y: auto;
max-height: calc(100vh - 500px); /* Açıklama için daha fazla alan bırak */
}
thead {
position: sticky;
top: 0;
background-color: var(--primary-color);
color: white;
z-index: 1;
}
th {
font-weight: 500;
text-transform: uppercase;
font-size: 12px;
letter-spacing: 0.5px;
}
tbody tr {
background-color: var(--table-bg-color);
transition: background-color 0.2s;
}
tbody tr:hover {
background-color: var(--hover-color);
cursor: move;
}
tbody tr.dragging {
opacity: 0.5;
background-color: var(--hover-color);
}
td:first-child, th:first-child {
border-left: none;
width: 30px;
text-align: center;
}
.context-menu {
position: fixed;
background-color: var(--table-bg-color);
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
z-index: 1000;
overflow: hidden;
}
.context-menu button {
display: block;
width: 100%;
padding: 8px 12px;
background-color: var(--table-bg-color);
border: none;
text-align: left;
cursor: pointer;
font-size: 12px;
color: var(--text-color);
transition: background-color 0.2s;
margin: 0;
}
.context-menu button:hover {
background-color: var(--hover-color);
}
tbody tr.drag-over {
background-color: rgba(74, 144, 226, 0.2);
box-shadow: 0 0 10px rgba(74, 144, 226, 0.5);
}
tbody tr.drag-over td {
border-top: 2px solid var(--primary-color);
border-bottom: 2px solid var(--primary-color);
}
tbody tr.drag-over td:first-child {
border-left: 2px solid var(--primary-color);
}
tbody tr.drag-over td:last-child {
border-right: 2px solid var(--primary-color);
}
.instructions {
margin-top: 16px;
font-size: 12px;
color: var(--text-color);
text-align: center;
background-color: var(--table-bg-color);
padding: 10px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
.instructions p {
margin: 4px 0;
}
.instructions a[href^="mailto:"] {
color: var(--primary-color);
text-decoration: none;
font-weight: bold;
transition: color 0.3s ease, text-decoration 0.3s ease;
}
.instructions a[href^="mailto:"]:hover {
color: var(--secondary-color);
text-decoration: underline;
}
.instructions a[href^="mailto:"]:before {
content: "\f0e0"; /* Font Awesome envelope icon */
font-family: "Font Awesome 5 Free";
font-weight: 900;
margin-right: 5px;
}
.conversion-progress {
display: flex;
align-items: center;
margin-top: 10px;
}
.conversion-progress progress {
flex-grow: 1;
height: 20px;
}
.conversion-progress span {
margin-left: 10px;
font-size: 14px;
}
.conversion-speed {
margin-top: 5px;
font-size: 14px;
}
progress {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 20px;
}
progress::-webkit-progress-bar {
background-color: var(--table-border-color);
border-radius: 4px;
}
progress::-webkit-progress-value {
background-color: var(--primary-color);
border-radius: 4px;
}
progress::-moz-progress-bar {
background-color: var(--primary-color);
border-radius: 4px;
}
.error-popup {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.error-content {
background-color: var(--table-bg-color);
padding: 20px;
border-radius: 8px;
max-width: 400px;
width: 100%;
}
.error-content h3 {
color: #ff4444;
margin-top: 0;
}
.error-content p {
margin-bottom: 20px;
}
</style>
<svelte:head>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="src/assets/css/all.min.css" rel="stylesheet">
</svelte:head>
|
- Stil bölümü, uygulamanın görünümünü özelleştirir.
Adım 3: Uygulamayı derleme ve çalıştırma
- Uygulamayı developer mod da çalıştırıp derlemeden önceki son görünümü ve düzenlemelerini yapmak için aşağıdaki komutu yazın
- Uygulama artık hazır ve istediğiniz gibiyse derleyip kullanıma başlayabilirsiniz.
Adım 4 : Uygulama için kurulum önerileri
- Macos işletim sistemi kullanıyorsanız ffmpeg kurulıumuna ihtiyacınız olacak bunun içinde aşağıdaki komut ile kurulum yapmanızı öneriyorum
- Linux ubuntu kullanıyorsanız öncelikle yerel webview kurulumu yapmak gerekecek daha sonra ffmpeg kurulumu gerekecek aşağıdaki komutları yazarak kurulumları yapalım
1
|
sudo apt install libgtk-3-dev libwebkit2gtk-4.0-dev
|
1
|
sudo apt install ffmpeg
|
- Windows kullanıyorsanız yapmanız gereken şey ffmpeg.exe ve ffprobe.exe yi indirerek uygulamanızın çalıştığı “bin” folder’ının içine kopyalamanız yeterli olacaktır, yada ffmpeg’i global de yani environment variables’da tanımlanızda yeterli olacaktır. ffmpeg kurlum dosyaları için FFmmpeg Resmi Sayfası
Sonuç
- Bu uyguluma ile herhangi bir video dosyasını FFmpeg kullanarak AV1(libsvvtav1) formatına çeviren bir uygulama yazmış olduk
- Uygulamayı yazarken Wails framework’u hakkında bilgi sahibi olduk
- Projenin kaynak kodlarının tamamına bu linkten erişebilirsiniz Github Av1-video-eoncoder