Skip to content

Commit 232df3a

Browse files
authored
Enable parallel MediaMTX playback (#3396)
* Enable parallel MediaMTX playback * Fix format * DRY * Switch to a custom multiwriter so that errors on one writer don't affect the others
1 parent 39db9b6 commit 232df3a

File tree

4 files changed

+89
-43
lines changed

4 files changed

+89
-43
lines changed

server/ai_live_video.go

Lines changed: 78 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,34 @@ func startTricklePublish(ctx context.Context, url *url.URL, params aiRequestPara
123123
clog.Infof(ctx, "trickle pub")
124124
}
125125

126+
type multiWriter struct {
127+
ctx context.Context
128+
writers []io.Writer
129+
isErrLogged bool
130+
}
131+
132+
func (t *multiWriter) Write(p []byte) (n int, err error) {
133+
success := false
134+
for _, w := range t.writers {
135+
bytesWritten, err := w.Write(p)
136+
if err != nil {
137+
if !t.isErrLogged {
138+
clog.Errorf(t.ctx, "multiWriter error %v", err)
139+
t.isErrLogged = true
140+
}
141+
} else {
142+
success = true
143+
n = bytesWritten
144+
}
145+
}
146+
if !success {
147+
// all writes failed, return the error
148+
return 0, err
149+
}
150+
151+
return n, nil
152+
}
153+
126154
func startTrickleSubscribe(ctx context.Context, url *url.URL, params aiRequestParams, onFistSegment func()) {
127155
// subscribe to the outputs and send them into LPMS
128156
subscriber := trickle.NewTrickleSubscriber(url.String())
@@ -131,15 +159,24 @@ func startTrickleSubscribe(ctx context.Context, url *url.URL, params aiRequestPa
131159
params.liveParams.stopPipeline(fmt.Errorf("error getting pipe for trickle-ffmpeg. url=%s %w", url, err))
132160
return
133161
}
162+
rMediaMTX, wMediaMTX, err := os.Pipe()
163+
if err != nil {
164+
params.liveParams.stopPipeline(fmt.Errorf("error getting pipe for MediaMTX trickle-ffmpeg. url=%s %w", url, err))
165+
return
166+
}
134167
ctx = clog.AddVal(ctx, "url", url.Redacted())
135168
ctx = clog.AddVal(ctx, "outputRTMPURL", params.liveParams.outputRTMPURL)
169+
ctx = clog.AddVal(ctx, "mediaMTXOutputRTMPURL", params.liveParams.mediaMTXOutputRTMPURL)
170+
171+
multiWriter := &multiWriter{ctx: ctx, writers: []io.Writer{w, wMediaMTX}}
136172

137173
// read segments from trickle subscription
138174
go func() {
139175
var err error
140176
firstSegment := true
141177

142178
defer w.Close()
179+
defer wMediaMTX.Close()
143180
retries := 0
144181
// we're trying to keep (retryPause x maxRetries) duration to fall within one output GOP length
145182
const retryPause = 300 * time.Millisecond
@@ -178,7 +215,7 @@ func startTrickleSubscribe(ctx context.Context, url *url.URL, params aiRequestPa
178215
seq := trickle.GetSeq(segment)
179216
clog.V(8).Infof(ctx, "trickle subscribe read data received seq=%d", seq)
180217

181-
n, err := copySegment(segment, w)
218+
n, err := copySegment(segment, multiWriter)
182219
if err != nil {
183220
params.liveParams.stopPipeline(fmt.Errorf("trickle subscribe error copying: %w", err))
184221
return
@@ -191,43 +228,50 @@ func startTrickleSubscribe(ctx context.Context, url *url.URL, params aiRequestPa
191228
}
192229
}()
193230

194-
go func() {
195-
defer func() {
196-
r.Close()
197-
if rec := recover(); rec != nil {
198-
// panicked, so shut down the stream and handle it
199-
err, ok := rec.(error)
200-
if !ok {
201-
err = errors.New("unknown error")
202-
}
203-
clog.Errorf(ctx, "LPMS panic err=%v", err)
204-
params.liveParams.stopPipeline(fmt.Errorf("LPMS panic %w", err))
205-
}
206-
}()
207-
for {
208-
clog.V(6).Infof(ctx, "Starting output rtmp")
209-
if !params.inputStreamExists() {
210-
clog.Errorf(ctx, "Stopping output rtmp stream, input stream does not exist.")
211-
break
212-
}
231+
// Studio Output ffmpeg process
232+
go ffmpegOutput(ctx, params.liveParams.outputRTMPURL, r, params)
213233

214-
cmd := exec.Command("ffmpeg",
215-
"-i", "pipe:0",
216-
"-c:a", "copy",
217-
"-c:v", "copy",
218-
"-f", "flv",
219-
params.liveParams.outputRTMPURL,
220-
)
221-
cmd.Stdin = r
222-
output, err := cmd.CombinedOutput()
223-
if err != nil {
224-
clog.Errorf(ctx, "Error sending RTMP out: %v", err)
225-
clog.Infof(ctx, "Process output: %s", output)
226-
return
234+
// MediaMTX Output ffmpeg process
235+
go ffmpegOutput(ctx, params.liveParams.mediaMTXOutputRTMPURL, rMediaMTX, params)
236+
}
237+
238+
func ffmpegOutput(ctx context.Context, outputUrl string, r io.ReadCloser, params aiRequestParams) {
239+
ctx = clog.AddVal(ctx, "rtmpOut", outputUrl)
240+
defer func() {
241+
r.Close()
242+
if rec := recover(); rec != nil {
243+
// panicked, so shut down the stream and handle it
244+
err, ok := rec.(error)
245+
if !ok {
246+
err = errors.New("unknown error")
227247
}
228-
time.Sleep(5 * time.Second)
248+
clog.Errorf(ctx, "LPMS panic err=%v", err)
249+
params.liveParams.stopPipeline(fmt.Errorf("LPMS panic %w", err))
229250
}
230251
}()
252+
for {
253+
clog.V(6).Infof(ctx, "Starting output rtmp")
254+
if !params.inputStreamExists() {
255+
clog.Errorf(ctx, "Stopping output rtmp stream, input stream does not exist.")
256+
break
257+
}
258+
259+
cmd := exec.Command("ffmpeg",
260+
"-i", "pipe:0",
261+
"-c:a", "copy",
262+
"-c:v", "copy",
263+
"-f", "flv",
264+
outputUrl,
265+
)
266+
cmd.Stdin = r
267+
output, err := cmd.CombinedOutput()
268+
clog.Infof(ctx, "Process output: %s", output)
269+
if err != nil {
270+
clog.Errorf(ctx, "Error sending RTMP out: %v", err)
271+
return
272+
}
273+
time.Sleep(5 * time.Second)
274+
}
231275
}
232276

233277
func copySegment(segment *http.Response, w io.Writer) (int64, error) {

server/ai_mediaserver.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,10 +428,12 @@ func (ls *LivepeerServer) StartLiveVideo() http.Handler {
428428
}
429429
// If auth webhook is set and returns an output URL, this will be replaced
430430
outputURL := qp.Get("rtmpOutput")
431+
432+
mediaMTXOutputURL := fmt.Sprintf("rtmp://%s/aiWebrtc/%s-out", remoteHost, streamName)
431433
if outputURL == "" {
432434
// re-publish to ourselves for now
433435
// Not sure if we want this to be permanent
434-
outputURL = fmt.Sprintf("rtmp://%s/%s-out", remoteHost, streamName)
436+
outputURL = mediaMTXOutputURL
435437
}
436438

437439
// convention to avoid re-subscribing to our own streams
@@ -556,6 +558,7 @@ func (ls *LivepeerServer) StartLiveVideo() http.Handler {
556558
liveParams: liveRequestParams{
557559
segmentReader: ssr,
558560
outputRTMPURL: outputURL,
561+
mediaMTXOutputRTMPURL: mediaMTXOutputURL,
559562
stream: streamName,
560563
paymentProcessInterval: ls.livePaymentInterval,
561564
requestID: requestID,

server/ai_process.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,13 @@ func (a aiRequestParams) inputStreamExists() bool {
9797

9898
// For live video pipelines
9999
type liveRequestParams struct {
100-
segmentReader *media.SwitchableSegmentReader
101-
outputRTMPURL string
102-
stream string
103-
requestID string
104-
streamID string
105-
pipelineID string
100+
segmentReader *media.SwitchableSegmentReader
101+
outputRTMPURL string
102+
mediaMTXOutputRTMPURL string
103+
stream string
104+
requestID string
105+
streamID string
106+
pipelineID string
106107

107108
paymentProcessInterval time.Duration
108109

server/auth.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,6 @@ type AIAuthRequest struct {
109109

110110
// Gateway host
111111
GatewayHost string `json:"gateway_host"`
112-
113-
// TODO not sure what params we need yet
114112
}
115113

116114
// Contains the configuration parameters for this AI job

0 commit comments

Comments
 (0)