package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws/credentials"
"math/rand"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
ffmpeg "github.com/u2takey/ffmpeg-go"
)
const CLOUD_PREFIX string = "https://s.repetitor.ru"
const OUTPUT_CLOUD_PREFIX string = "https://s.repetitor.ru/video/encode/"
const RELATIVE_CLOUD_PATH string = "/video/encode/"
const VIDEO_WIDTH = 1280
const VIDEO_HEIGHT = 720
// VideoMetadata содержит информацию о ширине и высоте видео
type VideoMetadata struct {
Streams []struct {
Width int `json:"width"`
Height int `json:"height"`
} `json:"streams"`
}
type RequestBody struct {
Url string
}
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
func randomString(n int) string {
sb := strings.Builder{}
sb.Grow(n)
for i := 0; i < n; i++ {
sb.WriteByte(charset[rand.Intn(len(charset))])
}
return sb.String()
}
func main() {
rand.Seed(time.Now().UnixNano())
// Load .env file
SetConfig()
// Retrieve configurations from environment variables
accessKey := os.Getenv("AWS_ACCESS_KEY_ID")
secretKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
region := os.Getenv("AWS_DEFAULT_REGION")
endpoint := os.Getenv("AWS_ENDPOINT_URL")
bucketName := os.Getenv("AWS_S3_BUCKET_NAME")
mode := os.Getenv("GIN_MODE")
port := os.Getenv("SERVER_PORT")
// Validate required AWS configurations
if accessKey == "" || secretKey == "" || region == "" || endpoint == "" || bucketName == "" {
log.Fatal("Missing required environment variables for AWS configuration")
}
// Set Gin mode
if mode == "release" {
gin.SetMode(gin.ReleaseMode)
} else {
gin.SetMode(gin.DebugMode)
}
// Default port if not set
if port == "" {
port = "8080"
}
// Create AWS session with custom endpoint
cfg := &aws.Config{
Region: aws.String(region),
Endpoint: aws.String(endpoint),
Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""),
}
sess, err := session.NewSession(cfg)
if err != nil {
log.Fatalf("Failed to create AWS session: %v", err)
}
client := s3.New(sess)
// Router setup
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
router.POST("/decode/", AuthMiddleware(),
func(c *gin.Context) {
var requestBody RequestBody
if err := c.BindJSON(&requestBody); err != nil {
log.Fatalf("Can't bind request body: %s", err)
}
url, _ := strings.CutPrefix(
requestBody.Url,
CLOUD_PREFIX,
)
splitUrl := strings.Split(url, "/")
filename := splitUrl[len(splitUrl)-1]
log.Printf("url: %s", url)
log.Printf("filename: %s", filename)
if url == "" {
c.JSON(http.StatusBadRequest, gin.H{
"message": "Url is required",
})
return
}
log.Printf("Decoding video: %s...", url)
outputFilename := CodeVideo(client, url, filename)
log.Printf("Work Done!")
c.IndentedJSON(http.StatusCreated, gin.H{
"url": fmt.Sprintf("%s%s", OUTPUT_CLOUD_PREFIX, outputFilename),
})
})
log.Printf("Server started on port: %s, in mode: %s", port, mode)
router.Run()
}
func SetConfig() {
err := godotenv.Load()
if err != nil {
log.Fatalf("Config file not found with error: %s", err)
}
log.Printf("Config file loaded: %s", ".env")
}
// AuthMiddleware проверяет secret-key из заголовка Authorization
func AuthMiddleware() gin.HandlerFunc {
secretKey := os.Getenv("SECRET_KEY")
if secretKey == "" {
log.Fatal("Secret key not found")
}
return func(c *gin.Context) {
// Извлекаем заголовок Authorization
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
c.Abort()
return
}
// Проверяем, совпадает ли ключ
if authHeader != secretKey {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid secret key"})
c.Abort()
return
}
// Если ключ валиден, продолжаем выполнение
c.Next()
}
}
// GetVideo загружает видео из S3 и сохраняет его во временный файл
func GetVideo(client *s3.S3, url, filename string) string {
bucket := os.Getenv("AWS_S3_BUCKET_NAME")
response, err := client.GetObject(&s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(fmt.Sprintf("/%s", url)),
})
if err != nil {
log.Fatalf("Failed to download file from S3: %v", err)
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("Failed to close response body: %v", err)
}
}(response.Body)
tempFile, err := os.Create(fmt.Sprintf("downloaded_%s_%s", randomString(5), filename))
if err != nil {
log.Fatalf("Failed to create temporary file: %v", err)
}
defer func(tempFile *os.File) {
err := tempFile.Close()
if err != nil {
log.Fatalf("Failed to close temporary file: %v", err)
}
}(tempFile)
_, err = io.Copy(tempFile, response.Body)
if err != nil {
log.Fatalf("Failed to save file locally: %v", err)
}
log.Printf("File downloaded and saved to: %s", tempFile.Name())
return tempFile.Name()
}
// UploadVideo загружает обработанное видео обратно в S3
func UploadVideo(client *s3.S3, localFile string) {
bucket := os.Getenv("AWS_S3_BUCKET_NAME")
// Открываем локальный файл
file, err := os.Open(localFile)
if err != nil {
log.Fatalf("Failed to open local file: %v", err)
}
defer func(file *os.File) {
err := file.Close()
if err != nil {
log.Fatalf("Failed to close local file: %v", err)
}
}(file) // Закрываем файл после завершения функции
// Загружаем файл в S3 напрямую из потока
_, err = client.PutObject(&s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(fmt.Sprintf("%s%s", RELATIVE_CLOUD_PATH, localFile)),
Body: file, // Передаем поток файла напрямую
ContentType: aws.String(http.DetectContentType([]byte(localFile))),
ContentDisposition: aws.String("attachment"),
})
if err != nil {
log.Fatalf("Failed to upload file to S3: %v", err)
}
log.Printf("File uploaded to S3 with key: video/encode/%s", localFile)
}
func splitByLastDot(s string) string {
lastDotIndex := strings.LastIndex(s, ".")
if lastDotIndex == -1 {
return s
}
firstPart := s[:lastDotIndex]
return firstPart
}
// CodeVideo обрабатывает видео и сохраняет его обратно в S3
func CodeVideo(client *s3.S3, url, filename string) string {
// Скачиваем видео из S3
inputFile := GetVideo(client, url, filename)
// Создаем временный файл для обработанного видео
outputFile, err := os.Create(filename)
if err != nil {
log.Fatalf("Failed to create temporary output file: %v", err)
return ""
}
outputFileName := splitByLastDot(outputFile.Name()) + randomString(5) + ".mp4"
// Извлекаем ширину и высоту видео
// width, height, err := getVideoDimensions()
if err != nil {
log.Fatalf("Ошибка извлечения размеров видео: %v", err)
return ""
}
cropParams, err := detectCropParams(inputFile)
if err != nil {
log.Printf("Crop detection failed: %v. Proceeding without cropping.", err)
return ""
} else {
log.Printf("Detected crop parameters: %s", cropParams)
}
croppedFile := "cropped_temp" + randomString(5) + ".mp4"
if cropParams != "" {
err = ffmpeg.Input(inputFile).
Filter("crop", ffmpeg.Args{cropParams}).
Output(croppedFile, ffmpeg.KwArgs{"c:v": "libx264", "c:a": "copy", "map": "0:a"}).
OverWriteOutput().
Run()
if err != nil {
log.Fatalf("Не удалось применить обрезку: %v", err)
return ""
}
log.Println("Видео успешно обрезано и сохранено во временный файл:", croppedFile)
} else {
// Если обрезка не требуется, используем исходный файл
croppedFile = inputFile
}
width, height, err := getVideoDimensions(croppedFile)
if err != nil {
log.Fatalf("Video Resolution can't be got, err: %s", err)
return ""
}
// Рассчитываем фильтр масштабирования
log.Printf("resolution: %d:%d", width, height)
scaleFilter := calculateScaleFilter(width, height)
err = ffmpeg.Input(croppedFile).
Filter("scale", ffmpeg.Args{scaleFilter}).
Output(outputFileName, ffmpeg.KwArgs{
"c:v": "libx264", // H.264 codec
"pix_fmt": "yuv420p", // compatible pixel format
"preset": "fast",
"movflags": "+faststart", // essential for browser playback
"c:a": "aac", // re-encode audio to AAC
"map": "0:a",
"b:a": "128k", // audio bitrate
}).
OverWriteOutput().
Run()
if err != nil {
log.Fatalf("Video encoding error: %v", err)
return ""
}
log.Printf("Encoding completed successfully. Processed file saved to: %s", outputFileName)
// Загружаем обработанное видео обратно в S3
UploadVideo(client, outputFileName)
// Удаляем временные файлы
err = os.Remove(inputFile)
if err != nil {
log.Printf("Failed to remove temporary input file: %v", err)
return ""
}
err = os.Remove(outputFileName)
if err != nil {
log.Printf("Failed to remove temporary output file: %v", err)
return ""
}
// Удаляем временный файл (если он был создан)
if cropParams != "" {
err = exec.Command("rm", croppedFile).Run()
if err != nil {
log.Printf("Не удалось удалить временный файл: %v", err)
return ""
} else {
log.Println("Временный файл удален:", croppedFile)
}
}
return outputFileName
}
// Function to detect crop parameters using ffmpeg's cropdetect filter
func detectCropParams(inputFile string) (string, error) {
// Run ffmpeg to detect crop parameters
cmd := exec.Command(
"ffmpeg",
"-i", inputFile,
"-vf", "cropdetect",
"-f", "null", "-",
)
// Capture the output
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("failed to detect crop parameters: %v", err)
}
// Parse the output to extract crop parameters
cropParams := parseCropParams(string(output))
if cropParams == "" {
return "", fmt.Errorf("no crop parameters detected")
}
return cropParams, nil
}
// Function to parse crop parameters from ffmpeg output
func parseCropParams(output string) string {
lines := strings.Split(output, "\n")
for _, line := range lines {
if strings.Contains(line, "crop=") {
parts := strings.Split(line, "crop=")
if len(parts) > 1 {
return strings.TrimSpace(parts[1])
}
}
}
return ""
}
// getVideoDimensions использует ffprobe для получения размеров видео
func getVideoDimensions(inputFile string) (int, int, error) {
cmd := exec.Command("ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
"stream=width,height", "-of", "json", inputFile)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
log.Fatalf("ffprobe error: %s", err)
}
// Парсим JSON-ответ
var metadata VideoMetadata
if err := json.Unmarshal(out.Bytes(), &metadata); err != nil {
log.Fatalf("parse JSON error: %s", err)
}
// Проверяем, что данные о ширине и высоте найдены
if len(metadata.Streams) == 0 || metadata.Streams[0].Width == 0 || metadata.Streams[0].Height == 0 {
log.Fatalf("get video resolution error")
}
return metadata.Streams[0].Width, metadata.Streams[0].Height, nil
}
// calculateScaleFilter рассчитывает фильтр масштабирования
func calculateScaleFilter(width, height int) string {
var targetWidth, targetHeight int
if width <= targetWidth && height <= targetHeight {
return fmt.Sprintf("%d:%d", width, height)
}
if width > height { // Горизонтальное видео
log.Printf("Video is Horizontal")
targetWidth = VIDEO_WIDTH
targetHeight = VIDEO_HEIGHT
log.Printf("Target resolution: %d:%d", targetWidth, targetHeight)
diff_width := width - targetWidth // 1 (width_initial-width_target)
log.Printf("difference in initial width and target: %d", diff_width)
var proportion float64 = float64(diff_width) / float64(width) // 2 ([1] / width)
log.Printf("how much we need to scale video to reach target resolution: %f", proportion)
diff_height := proportion * float64(height) // 3 (height_initial * [2])
log.Printf("how much we need to subtract from initial height to save proportion: %f", diff_height)
newHeight := ensureEven(height - int(diff_height)) // 4 (height_initial - [3])
log.Printf("subtract proportioned height from initial: %d", newHeight)
log.Printf("%d:%d", targetWidth, newHeight)
return fmt.Sprintf("%d:%d", targetWidth, newHeight)
} else { // Вертикальное видео
log.Printf("Video is Vertical")
targetWidth = VIDEO_HEIGHT
targetHeight = VIDEO_WIDTH
log.Printf("Target resolution: %d:%d", targetWidth, targetHeight)
diff_height := height - targetHeight // 1 (height_initial-height_target)
log.Printf("difference in initial height and target: %d", diff_height)
var proportion_coefficient float64 = float64(diff_height) / float64(height) // 2 ([1] / height)
log.Printf("how much we need to scale video to reach target resolution: %f", proportion_coefficient)
diff_width := float64(width) * proportion_coefficient // 3 (width_initial * [2])
log.Printf("how much we need to subtract from initial width to save proportion: %f", diff_width)
newWidth := ensureEven(width - int(diff_width)) // 4 (width_initial - [3])
log.Printf("subtract proportioned width from initial: %d", newWidth)
log.Printf("%d:%d", targetWidth, newWidth)
return fmt.Sprintf("%d:%d", newWidth, targetHeight)
}
}
// ensureEven гарантирует, что число чётное
func ensureEven(value int) int {
if value%2 != 0 {
return value + 1
}
return value
}