Files
huly-platform/foundations/stream/internal/pkg/mediaconvert/stream.go
T
Denis Bykhov 6e77cbb762 Add 'foundations/stream/' from commit 'b7b6930b0992ae48f08d37745d5e6e5e26517337'
git-subtree-dir: foundations/stream
git-subtree-mainline: 328a7dc269
git-subtree-split: b7b6930b09
2025-11-26 23:06:55 +05:00

203 lines
5.7 KiB
Go

// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
// Package mediaconvert provides types and functions for video transcoding
package mediaconvert
import (
"context"
"io"
"sync"
"github.com/pkg/errors"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"github.com/hcengineering/stream/internal/pkg/sharedpipe"
"github.com/hcengineering/stream/internal/pkg/storage"
"github.com/hcengineering/stream/internal/pkg/tracing"
"github.com/tus/tusd/v2/pkg/handler"
"go.uber.org/zap"
)
// Stream manages client's input and transcodes it based on the passed configuration
type Stream struct {
logger *zap.Logger
info handler.FileInfo
writer *sharedpipe.Writer
reader *sharedpipe.Reader
storage storage.Storage
multipart *MultipartUpload
done chan struct{}
}
var _ handler.Upload = (*Stream)(nil)
var _ handler.ConcatableUpload = (*Stream)(nil)
var _ handler.TerminatableUpload = (*Stream)(nil)
var _ handler.LengthDeclarableUpload = (*Stream)(nil)
// WriteChunk is called when client sends a chunk of raw data
func (w *Stream) WriteChunk(ctx context.Context, _ int64, src io.Reader) (int64, error) {
ctx, span := tracer.Start(ctx, "WriteChunk", trace.WithAttributes(
attribute.String("workspace", w.info.MetaData["workspace"]),
attribute.String("upload_id", w.info.ID),
))
defer span.End()
w.logger.Debug("Write Chunk start", zap.Int64("offset", w.info.Offset))
data, err := io.ReadAll(src)
if err != nil {
return 0, err
}
// write into pipeline for transcoding
written, err := w.writer.Write(data)
if err != nil {
return int64(written), err
}
n := int64(len(data))
w.info.Offset += n
if w.multipart != nil {
if writeErr := w.multipart.Write(ctx, data); writeErr != nil {
w.logger.Error("multipart upload part failed", zap.Error(writeErr))
return n, writeErr
}
}
w.logger.Debug("write chunk end", zap.Int64("offset", w.info.Offset), zap.Error(err))
return n, nil
}
// DeclareLength sets length of the video input
func (w *Stream) DeclareLength(ctx context.Context, length int64) error {
w.info.Size = length
w.info.SizeIsDeferred = false
w.logger.Debug("DeclareLength", zap.Int64("size", length), zap.Bool("SizeIsDeferred", w.info.SizeIsDeferred))
return nil
}
// GetInfo returns info about transcoing status
func (w *Stream) GetInfo(ctx context.Context) (handler.FileInfo, error) {
w.logger.Debug("GetInfo is executed")
return w.info, nil
}
// GetReader returns Stream's bytes stream
func (w *Stream) GetReader(ctx context.Context) (io.ReadCloser, error) {
w.logger.Debug("GetReader is executed, creating current reader...")
return w.reader, nil
}
// Terminate calls when upload has failed
func (w *Stream) Terminate(ctx context.Context) error {
ctx, span := tracer.Start(ctx, "Terminate", trace.WithAttributes(
attribute.String("workspace", w.info.MetaData["workspace"]),
attribute.String("upload_id", w.info.ID),
))
defer span.End()
w.logger.Debug("terminate upload")
// Close the writer first to signal EOF to all readers
if err := w.writer.Close(); err != nil {
tracing.RecordError(span, err)
w.logger.Error("failed to close writer", zap.Error(err))
return err
}
var wg sync.WaitGroup
// cancel multipart upload if in progress
if w.multipart != nil {
wg.Add(1)
go func() {
defer wg.Done()
if err := w.multipart.Terminate(ctx); err != nil {
w.logger.Error("multipart upload cancel failed", zap.Error(err))
}
}()
}
wg.Wait()
// Signal that the stream is done
close(w.done)
return nil
}
// ConcatUploads calls when upload resumed after fail
func (w *Stream) ConcatUploads(ctx context.Context, partialUploads []handler.Upload) error {
w.logger.Debug("ConcatUploads was executed, it's not implemented")
//
// TODO: load raw source from the Buckup bucket, terminate all Streams with same ID and start process again.
//
return errors.New("not implemented")
}
// FinishUpload calls when upload finished without errors on the client side
func (w *Stream) FinishUpload(ctx context.Context) error {
ctx, span := tracer.Start(ctx, "FinishUpload", trace.WithAttributes(
attribute.String("workspace", w.info.MetaData["workspace"]),
attribute.String("upload_id", w.info.ID),
))
defer span.End()
w.logger.Debug("finish upload")
// Close the writer first to signal EOF to all readers
if err := w.writer.Close(); err != nil {
tracing.RecordError(span, err)
w.logger.Error("failed to close writer", zap.Error(err))
return err
}
var wg sync.WaitGroup
var completeErr error
// finalize raw multipart stream if supported
if w.multipart != nil {
wg.Add(1)
go func() {
defer wg.Done()
if err := w.multipart.Complete(ctx); err != nil {
w.logger.Error("multipart upload complete failed", zap.Error(err))
tracing.RecordError(span, err)
completeErr = err
return
}
}()
}
wg.Wait()
// Signal that the stream is done
close(w.done)
return completeErr
}
// AsConcatableUpload returns tusd handler.ConcatableUpload
func (s *StreamCoordinator) AsConcatableUpload(upload handler.Upload) handler.ConcatableUpload {
s.logger.Debug("AsConcatableUpload is executed")
return upload.(*Stream)
}
func (w *Stream) start() error {
defer w.logger.Debug("start done")
w.reader = w.writer.Transpile()
return nil
}