v.0.0.3 Изменена логика обрезки видео и его сохранения в s3
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
7bfc7eb9a8
commit
272c76909b
@ -3,6 +3,7 @@ package ffmpeg
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
@ -12,15 +13,46 @@ 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) {
|
||||
var thumbnailData []byte
|
||||
|
||||
err := vp.withTempFile("thumbnail_*.mp4", videoData, func(inputPath string) error {
|
||||
cmd := exec.Command("ffmpeg",
|
||||
"-i", "pipe:0",
|
||||
"-i", inputPath,
|
||||
"-ss", "00:00:01",
|
||||
"-vframes", "1",
|
||||
"-q:v", "2",
|
||||
@ -29,48 +61,70 @@ func (vp *videoProcessor) GenerateThumbnail(videoData []byte) ([]byte, error) {
|
||||
"pipe:1",
|
||||
)
|
||||
|
||||
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 {
|
||||
return nil, fmt.Errorf("ffmpeg failed: %s, error: %w", stderr.String(), err)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("ffmpeg failed: %s, error: %w", stderr.String(), err)
|
||||
}
|
||||
|
||||
if output.Len() == 0 {
|
||||
return nil, fmt.Errorf("thumbnail generation produced empty output")
|
||||
}
|
||||
thumbnailData = output.Bytes()
|
||||
return nil
|
||||
})
|
||||
|
||||
return output.Bytes(), 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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
thumbnailChan <- thumbnailData
|
||||
}()
|
||||
|
||||
// 2. Ждем результаты обработки
|
||||
var trimmedVideo []byte
|
||||
var thumbnailData []byte
|
||||
|
||||
select {
|
||||
case trimmedVideo = <-trimmedVideoChan:
|
||||
case err := <-trimmedErrChan:
|
||||
return nil, err
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("%s: operation cancelled", op)
|
||||
}
|
||||
|
||||
select {
|
||||
case thumbnailData = <-thumbnailChan:
|
||||
case err := <-thumbnailErrChan:
|
||||
return nil, err
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("%s: operation cancelled", op)
|
||||
}
|
||||
|
||||
// 3. Загружаем обрезанное видео и превью в S3
|
||||
trimmedVideoURL, err := s.storage.UploadVideo(ctx, trimmedVideo, req.FileName, req.UserID)
|
||||
if err != nil {
|
||||
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 {
|
||||
thumbnailErrChan <- fmt.Errorf("%s: failed to upload thumbnail: %w", op, err)
|
||||
return
|
||||
}
|
||||
thumbnailChan <- thumbnailURL
|
||||
}()
|
||||
|
||||
// 3. Ждем результаты операций
|
||||
var videoURL, thumbnailURL string
|
||||
|
||||
// Ждем загрузки видео
|
||||
select {
|
||||
case videoURL = <-videoURLChan:
|
||||
case err := <-videoErrChan:
|
||||
return nil, err
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("%s: operation cancelled", op)
|
||||
s.storage.DeleteVideo(ctx, trimmedVideoURL)
|
||||
return nil, fmt.Errorf("%s: failed to upload thumbnail: %w", op, err)
|
||||
}
|
||||
|
||||
// Ждем генерации превью
|
||||
select {
|
||||
case thumbnailURL = <-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)
|
||||
// 4. Модерируем обрезанное видео
|
||||
videoAllowed, err := s.modClient.CheckVideoUrl(ctx, trimmedVideoURL)
|
||||
if err != nil {
|
||||
s.storage.DeleteVideo(ctx, videoURL)
|
||||
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)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user