mirror of
https://github.com/livekit/livekit.git
synced 2026-03-29 09:19:53 +00:00
192 lines
4.8 KiB
Go
192 lines
4.8 KiB
Go
// Copyright 2023 LiveKit, Inc.
|
|
//
|
|
// Licensed under the Apache 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
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-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 client
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/pion/webrtc/v4"
|
|
"github.com/pion/webrtc/v4/pkg/media"
|
|
"github.com/pion/webrtc/v4/pkg/media/h264reader"
|
|
"github.com/pion/webrtc/v4/pkg/media/ivfreader"
|
|
"github.com/pion/webrtc/v4/pkg/media/oggreader"
|
|
|
|
"github.com/livekit/protocol/codecs/mime"
|
|
"github.com/livekit/protocol/logger"
|
|
)
|
|
|
|
type TrackWriter interface {
|
|
Start() error
|
|
Stop()
|
|
}
|
|
|
|
// Writes a file to an RTP track.
|
|
// makes it easier to debug and create RTP streams
|
|
type trackWriter struct {
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
track *webrtc.TrackLocalStaticSample
|
|
filePath string
|
|
mime mime.MimeType
|
|
|
|
ogg *oggreader.OggReader
|
|
ivfheader *ivfreader.IVFFileHeader
|
|
ivf *ivfreader.IVFReader
|
|
h264 *h264reader.H264Reader
|
|
}
|
|
|
|
func NewTrackWriter(ctx context.Context, track *webrtc.TrackLocalStaticSample, filePath string) TrackWriter {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
return &trackWriter{
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
track: track,
|
|
filePath: filePath,
|
|
mime: mime.NormalizeMimeType(track.Codec().MimeType),
|
|
}
|
|
}
|
|
|
|
func (w *trackWriter) Start() error {
|
|
if w.filePath == "" {
|
|
go w.writeNull()
|
|
return nil
|
|
}
|
|
|
|
file, err := os.Open(w.filePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
logger.Debugw(
|
|
"starting track writer",
|
|
"trackID", w.track.ID(),
|
|
"mime", w.mime,
|
|
)
|
|
switch w.mime {
|
|
case mime.MimeTypeOpus:
|
|
w.ogg, _, err = oggreader.NewWith(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
go w.writeOgg()
|
|
case mime.MimeTypeVP8:
|
|
w.ivf, w.ivfheader, err = ivfreader.NewWith(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
go w.writeVP8()
|
|
case mime.MimeTypeH264:
|
|
w.h264, err = h264reader.NewReader(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
go w.writeH264()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *trackWriter) Stop() {
|
|
w.cancel()
|
|
}
|
|
|
|
func (w *trackWriter) writeNull() {
|
|
defer w.onWriteComplete()
|
|
sample := media.Sample{Data: []byte{0x0, 0xff, 0xff, 0xff, 0xff}, Duration: 30 * time.Millisecond}
|
|
h264Sample := media.Sample{Data: []byte{0x00, 0x00, 0x00, 0x01, 0x7, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x8, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x5, 0xff, 0xff, 0xff, 0xff}, Duration: 30 * time.Millisecond}
|
|
for {
|
|
select {
|
|
case <-time.After(20 * time.Millisecond):
|
|
if w.mime == mime.MimeTypeH264 {
|
|
w.track.WriteSample(h264Sample)
|
|
} else {
|
|
w.track.WriteSample(sample)
|
|
}
|
|
case <-w.ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *trackWriter) writeOgg() {
|
|
// Keep track of last granule, the difference is the amount of samples in the buffer
|
|
var lastGranule uint64
|
|
for {
|
|
if w.ctx.Err() != nil {
|
|
return
|
|
}
|
|
pageData, pageHeader, err := w.ogg.ParseNextPage()
|
|
if err == io.EOF {
|
|
logger.Debugw("all audio samples parsed and sent")
|
|
w.onWriteComplete()
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
logger.Errorw("could not parse ogg page", err)
|
|
return
|
|
}
|
|
|
|
// The amount of samples is the difference between the last and current timestamp
|
|
sampleCount := float64(pageHeader.GranulePosition - lastGranule)
|
|
lastGranule = pageHeader.GranulePosition
|
|
sampleDuration := time.Duration((sampleCount/48000)*1000) * time.Millisecond
|
|
|
|
if err = w.track.WriteSample(media.Sample{Data: pageData, Duration: sampleDuration}); err != nil {
|
|
logger.Errorw("could not write sample", err)
|
|
return
|
|
}
|
|
|
|
time.Sleep(sampleDuration)
|
|
}
|
|
}
|
|
|
|
func (w *trackWriter) writeVP8() {
|
|
// Send our video file frame at a time. Pace our sending such that we send it at the same speed it should be played back as.
|
|
// This isn't required since the video is timestamped, but we will such much higher loss if we send all at once.
|
|
sleepTime := time.Millisecond * time.Duration((float32(w.ivfheader.TimebaseNumerator)/float32(w.ivfheader.TimebaseDenominator))*1000)
|
|
for {
|
|
if w.ctx.Err() != nil {
|
|
return
|
|
}
|
|
frame, _, err := w.ivf.ParseNextFrame()
|
|
if err == io.EOF {
|
|
logger.Debugw("all video frames parsed and sent")
|
|
w.onWriteComplete()
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
logger.Errorw("could not parse VP8 frame", err)
|
|
return
|
|
}
|
|
|
|
time.Sleep(sleepTime)
|
|
if err = w.track.WriteSample(media.Sample{Data: frame, Duration: time.Second}); err != nil {
|
|
logger.Errorw("could not write sample", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *trackWriter) writeH264() {
|
|
// TODO: this is harder
|
|
}
|
|
|
|
func (w *trackWriter) onWriteComplete() {
|
|
}
|