v.0.0.3 Изменена логика обрезки видео и его сохранения в s3
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
admin 2025-09-02 23:17:24 +03:00
parent 7bfc7eb9a8
commit 272c76909b
2 changed files with 122 additions and 71 deletions

View File

@ -3,6 +3,7 @@ package ffmpeg
import (
"bytes"
"fmt"
"os"
"os/exec"
)
@ -12,65 +13,118 @@ type VideoProcessor interface {
}
type videoProcessor struct {
tempDir string // опционально: каталог для временных файлов
}
func NewVideoProcessor() VideoProcessor {
return &videoProcessor{}
return &videoProcessor{
tempDir: os.TempDir(), // используем системный temp каталог
}
}
func (vp *videoProcessor) withTempFile(pattern string, data []byte, fn func(filename string) error) error {
// Создаем временный файл
file, err := os.CreateTemp(vp.tempDir, pattern)
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
tempPath := file.Name()
// Гарантируем удаление файла
defer func() {
if _, err := os.Stat(tempPath); err == nil {
os.Remove(tempPath)
}
}()
// Записываем данные
if _, err := file.Write(data); err != nil {
return fmt.Errorf("failed to write to temp file: %w", err)
}
file.Close()
// Вызываем функцию обработки
return fn(tempPath)
}
func (vp *videoProcessor) GenerateThumbnail(videoData []byte) ([]byte, error) {
cmd := exec.Command("ffmpeg",
"-i", "pipe:0",
"-ss", "00:00:01",
"-vframes", "1",
"-q:v", "2",
"-f", "image2pipe",
"-c:v", "mjpeg",
"pipe:1",
)
var thumbnailData []byte
cmd.Stdin = bytes.NewReader(videoData)
var output bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &output
cmd.Stderr = &stderr
err := vp.withTempFile("thumbnail_*.mp4", videoData, func(inputPath string) error {
cmd := exec.Command("ffmpeg",
"-i", inputPath,
"-ss", "00:00:01",
"-vframes", "1",
"-q:v", "2",
"-f", "image2pipe",
"-c:v", "mjpeg",
"pipe:1",
)
err := cmd.Run()
if err != nil {
return nil, fmt.Errorf("ffmpeg failed: %s, error: %w", stderr.String(), err)
}
var output bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &output
cmd.Stderr = &stderr
if output.Len() == 0 {
return nil, fmt.Errorf("thumbnail generation produced empty output")
}
if err := cmd.Run(); err != nil {
return fmt.Errorf("ffmpeg failed: %s, error: %w", stderr.String(), err)
}
return output.Bytes(), nil
thumbnailData = output.Bytes()
return nil
})
return thumbnailData, err
}
func (vp *videoProcessor) TrimVideo(videoData []byte, maxDuration int) ([]byte, error) {
// Просто обрезаем видео без проверки длительности
// 1. Создаем временный файл для входного видео
inputFile, err := os.CreateTemp(vp.tempDir, "trim_input_*.mp4")
if err != nil {
return nil, fmt.Errorf("failed to create temp file: %w", err)
}
defer os.Remove(inputFile.Name())
defer inputFile.Close()
// 2. Записываем видео данные во временный файл
if _, err := inputFile.Write(videoData); err != nil {
return nil, fmt.Errorf("failed to write to temp file: %w", err)
}
inputFile.Close()
// 3. Создаем временный файл для выходного видео
outputFile, err := os.CreateTemp(vp.tempDir, "trim_output_*.mp4")
if err != nil {
return nil, fmt.Errorf("failed to create output temp file: %w", err)
}
defer os.Remove(outputFile.Name())
defer outputFile.Close()
// 4. Выполняем обрезку
cmd := exec.Command("ffmpeg",
"-i", "pipe:0",
"-i", inputFile.Name(),
"-t", fmt.Sprintf("%d", maxDuration),
"-c", "copy",
"-f", "mp4",
"pipe:1",
"-y", // overwrite output file
outputFile.Name(),
)
cmd.Stdin = bytes.NewReader(videoData)
var output bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &output
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("ffmpeg trim failed: %s, error: %w", stderr.String(), err)
}
if output.Len() == 0 {
// 5. Читаем обрезанное видео
trimmedData, err := os.ReadFile(outputFile.Name())
if err != nil {
return nil, fmt.Errorf("failed to read trimmed video: %w", err)
}
if len(trimmedData) == 0 {
return nil, fmt.Errorf("trimmed video is empty")
}
return output.Bytes(), nil
return trimmedData, nil
}

View File

@ -43,76 +43,73 @@ func NewClipService(repo repository.ClipRepository, storage storage.Storage, pro
func (s *clipService) CreateClip(ctx context.Context, req domain.CreateClipRequest) (*domain.Clip, error) {
const op = "service/clipService.CreateClip"
// 1. Обрезаем видео до 30 секунд в памяти
trimmedVideoData, err := s.videoProcessor.TrimVideo(req.VideoData, 30)
if err != nil {
return nil, fmt.Errorf("%s: failed to trim video: %w", op, err)
}
// 2. Параллельно выполняем операции
videoURLChan := make(chan string, 1)
videoErrChan := make(chan error, 1)
thumbnailChan := make(chan string, 1)
// 1. Параллельно обрабатываем видео и генерируем превью
trimmedVideoChan := make(chan []byte, 1)
trimmedErrChan := make(chan error, 1)
thumbnailChan := make(chan []byte, 1)
thumbnailErrChan := make(chan error, 1)
// Горутина для загрузки видео в S3
// Горутина для обрезки видео
go func() {
videoURL, err := s.storage.UploadVideo(ctx, trimmedVideoData, req.FileName, req.UserID)
trimmedVideo, err := s.videoProcessor.TrimVideo(req.VideoData, 30)
if err != nil {
videoErrChan <- fmt.Errorf("%s: failed to upload video: %w", op, err)
trimmedErrChan <- fmt.Errorf("%s: failed to trim video: %w", op, err)
return
}
videoURLChan <- videoURL
trimmedVideoChan <- trimmedVideo
}()
// Горутина для генерации превью
go func() {
thumbnailData, err := s.videoProcessor.GenerateThumbnail(trimmedVideoData)
thumbnailData, err := s.videoProcessor.GenerateThumbnail(req.VideoData)
if err != nil {
thumbnailErrChan <- fmt.Errorf("%s: failed to generate thumbnail: %w", op, err)
return
}
thumbnailURL, err := s.storage.UploadThumbnail(ctx, thumbnailData, req.UserID)
if err != nil {
thumbnailErrChan <- fmt.Errorf("%s: failed to upload thumbnail: %w", op, err)
return
}
thumbnailChan <- thumbnailURL
thumbnailChan <- thumbnailData
}()
// 3. Ждем результаты операций
var videoURL, thumbnailURL string
// 2. Ждем результаты обработки
var trimmedVideo []byte
var thumbnailData []byte
// Ждем загрузки видео
select {
case videoURL = <-videoURLChan:
case err := <-videoErrChan:
case trimmedVideo = <-trimmedVideoChan:
case err := <-trimmedErrChan:
return nil, err
case <-ctx.Done():
return nil, fmt.Errorf("%s: operation cancelled", op)
}
// Ждем генерации превью
select {
case thumbnailURL = <-thumbnailChan:
case thumbnailData = <-thumbnailChan:
case err := <-thumbnailErrChan:
s.storage.DeleteVideo(ctx, videoURL)
return nil, err
case <-ctx.Done():
s.storage.DeleteVideo(ctx, videoURL)
return nil, fmt.Errorf("%s: operation cancelled", op)
}
// 4. Модерируем обрезанное видео по URL
videoAllowed, err := s.modClient.CheckVideoUrl(ctx, videoURL)
// 3. Загружаем обрезанное видео и превью в S3
trimmedVideoURL, err := s.storage.UploadVideo(ctx, trimmedVideo, req.FileName, req.UserID)
if err != nil {
s.storage.DeleteVideo(ctx, videoURL)
return nil, fmt.Errorf("%s: failed to upload trimmed video: %w", op, err)
}
thumbnailURL, err := s.storage.UploadThumbnail(ctx, thumbnailData, req.UserID)
if err != nil {
s.storage.DeleteVideo(ctx, trimmedVideoURL)
return nil, fmt.Errorf("%s: failed to upload thumbnail: %w", op, err)
}
// 4. Модерируем обрезанное видео
videoAllowed, err := s.modClient.CheckVideoUrl(ctx, trimmedVideoURL)
if err != nil {
s.storage.DeleteVideo(ctx, trimmedVideoURL)
s.storage.DeleteThumbnail(ctx, thumbnailURL)
return nil, fmt.Errorf("%s: video moderation failed: %w", op, err)
}
if !videoAllowed {
s.storage.DeleteVideo(ctx, videoURL)
s.storage.DeleteVideo(ctx, trimmedVideoURL)
s.storage.DeleteThumbnail(ctx, thumbnailURL)
return nil, fmt.Errorf("%s: video rejected by moderation service", op)
}
@ -120,7 +117,7 @@ func (s *clipService) CreateClip(ctx context.Context, req domain.CreateClipReque
// 5. Создаем клип в БД
clip := &domain.Clip{
Title: req.Title,
VideoURL: videoURL,
VideoURL: trimmedVideoURL,
ThumbnailURL: thumbnailURL,
AuthorID: req.UserID,
LikesCount: 0,
@ -130,7 +127,7 @@ func (s *clipService) CreateClip(ctx context.Context, req domain.CreateClipReque
}
if err := s.repo.Create(ctx, clip); err != nil {
s.storage.DeleteVideo(ctx, videoURL)
s.storage.DeleteVideo(ctx, trimmedVideoURL)
s.storage.DeleteThumbnail(ctx, thumbnailURL)
return nil, fmt.Errorf("failed to create clip: %w", err)
}