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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -12,15 +13,46 @@ type VideoProcessor interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type videoProcessor struct {
|
type videoProcessor struct {
|
||||||
|
tempDir string // опционально: каталог для временных файлов
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewVideoProcessor() VideoProcessor {
|
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) {
|
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",
|
cmd := exec.Command("ffmpeg",
|
||||||
"-i", "pipe:0",
|
"-i", inputPath,
|
||||||
"-ss", "00:00:01",
|
"-ss", "00:00:01",
|
||||||
"-vframes", "1",
|
"-vframes", "1",
|
||||||
"-q:v", "2",
|
"-q:v", "2",
|
||||||
@ -29,48 +61,70 @@ func (vp *videoProcessor) GenerateThumbnail(videoData []byte) ([]byte, error) {
|
|||||||
"pipe:1",
|
"pipe:1",
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd.Stdin = bytes.NewReader(videoData)
|
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
cmd.Stdout = &output
|
cmd.Stdout = &output
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
err := cmd.Run()
|
if err := cmd.Run(); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("ffmpeg failed: %s, error: %w", stderr.String(), err)
|
||||||
return nil, fmt.Errorf("ffmpeg failed: %s, error: %w", stderr.String(), err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if output.Len() == 0 {
|
thumbnailData = output.Bytes()
|
||||||
return nil, fmt.Errorf("thumbnail generation produced empty output")
|
return nil
|
||||||
}
|
})
|
||||||
|
|
||||||
return output.Bytes(), nil
|
return thumbnailData, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (vp *videoProcessor) TrimVideo(videoData []byte, maxDuration int) ([]byte, error) {
|
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",
|
cmd := exec.Command("ffmpeg",
|
||||||
"-i", "pipe:0",
|
"-i", inputFile.Name(),
|
||||||
"-t", fmt.Sprintf("%d", maxDuration),
|
"-t", fmt.Sprintf("%d", maxDuration),
|
||||||
"-c", "copy",
|
"-c", "copy",
|
||||||
"-f", "mp4",
|
"-y", // overwrite output file
|
||||||
"pipe:1",
|
outputFile.Name(),
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd.Stdin = bytes.NewReader(videoData)
|
|
||||||
var output bytes.Buffer
|
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
cmd.Stdout = &output
|
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
err := cmd.Run()
|
if err := cmd.Run(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("ffmpeg trim failed: %s, error: %w", stderr.String(), err)
|
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 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) {
|
func (s *clipService) CreateClip(ctx context.Context, req domain.CreateClipRequest) (*domain.Clip, error) {
|
||||||
const op = "service/clipService.CreateClip"
|
const op = "service/clipService.CreateClip"
|
||||||
|
|
||||||
// 1. Обрезаем видео до 30 секунд в памяти
|
// 1. Параллельно обрабатываем видео и генерируем превью
|
||||||
trimmedVideoData, err := s.videoProcessor.TrimVideo(req.VideoData, 30)
|
trimmedVideoChan := make(chan []byte, 1)
|
||||||
if err != nil {
|
trimmedErrChan := make(chan error, 1)
|
||||||
return nil, fmt.Errorf("%s: failed to trim video: %w", op, err)
|
thumbnailChan := make(chan []byte, 1)
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Параллельно выполняем операции
|
|
||||||
videoURLChan := make(chan string, 1)
|
|
||||||
videoErrChan := make(chan error, 1)
|
|
||||||
thumbnailChan := make(chan string, 1)
|
|
||||||
thumbnailErrChan := make(chan error, 1)
|
thumbnailErrChan := make(chan error, 1)
|
||||||
|
|
||||||
// Горутина для загрузки видео в S3
|
// Горутина для обрезки видео
|
||||||
go func() {
|
go func() {
|
||||||
videoURL, err := s.storage.UploadVideo(ctx, trimmedVideoData, req.FileName, req.UserID)
|
trimmedVideo, err := s.videoProcessor.TrimVideo(req.VideoData, 30)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
videoURLChan <- videoURL
|
trimmedVideoChan <- trimmedVideo
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Горутина для генерации превью
|
// Горутина для генерации превью
|
||||||
go func() {
|
go func() {
|
||||||
thumbnailData, err := s.videoProcessor.GenerateThumbnail(trimmedVideoData)
|
thumbnailData, err := s.videoProcessor.GenerateThumbnail(req.VideoData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
thumbnailErrChan <- fmt.Errorf("%s: failed to generate thumbnail: %w", op, err)
|
thumbnailErrChan <- fmt.Errorf("%s: failed to generate thumbnail: %w", op, err)
|
||||||
return
|
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)
|
thumbnailURL, err := s.storage.UploadThumbnail(ctx, thumbnailData, req.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
thumbnailErrChan <- fmt.Errorf("%s: failed to upload thumbnail: %w", op, err)
|
s.storage.DeleteVideo(ctx, trimmedVideoURL)
|
||||||
return
|
return nil, fmt.Errorf("%s: failed to upload thumbnail: %w", op, err)
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ждем генерации превью
|
// 4. Модерируем обрезанное видео
|
||||||
select {
|
videoAllowed, err := s.modClient.CheckVideoUrl(ctx, trimmedVideoURL)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.storage.DeleteVideo(ctx, videoURL)
|
s.storage.DeleteVideo(ctx, trimmedVideoURL)
|
||||||
s.storage.DeleteThumbnail(ctx, thumbnailURL)
|
s.storage.DeleteThumbnail(ctx, thumbnailURL)
|
||||||
return nil, fmt.Errorf("%s: video moderation failed: %w", op, err)
|
return nil, fmt.Errorf("%s: video moderation failed: %w", op, err)
|
||||||
}
|
}
|
||||||
if !videoAllowed {
|
if !videoAllowed {
|
||||||
s.storage.DeleteVideo(ctx, videoURL)
|
s.storage.DeleteVideo(ctx, trimmedVideoURL)
|
||||||
s.storage.DeleteThumbnail(ctx, thumbnailURL)
|
s.storage.DeleteThumbnail(ctx, thumbnailURL)
|
||||||
return nil, fmt.Errorf("%s: video rejected by moderation service", op)
|
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. Создаем клип в БД
|
// 5. Создаем клип в БД
|
||||||
clip := &domain.Clip{
|
clip := &domain.Clip{
|
||||||
Title: req.Title,
|
Title: req.Title,
|
||||||
VideoURL: videoURL,
|
VideoURL: trimmedVideoURL,
|
||||||
ThumbnailURL: thumbnailURL,
|
ThumbnailURL: thumbnailURL,
|
||||||
AuthorID: req.UserID,
|
AuthorID: req.UserID,
|
||||||
LikesCount: 0,
|
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 {
|
if err := s.repo.Create(ctx, clip); err != nil {
|
||||||
s.storage.DeleteVideo(ctx, videoURL)
|
s.storage.DeleteVideo(ctx, trimmedVideoURL)
|
||||||
s.storage.DeleteThumbnail(ctx, thumbnailURL)
|
s.storage.DeleteThumbnail(ctx, thumbnailURL)
|
||||||
return nil, fmt.Errorf("failed to create clip: %w", err)
|
return nil, fmt.Errorf("failed to create clip: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user