@@ -123,6 +123,34 @@ func startTricklePublish(ctx context.Context, url *url.URL, params aiRequestPara
123
123
clog .Infof (ctx , "trickle pub" )
124
124
}
125
125
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
+
126
154
func startTrickleSubscribe (ctx context.Context , url * url.URL , params aiRequestParams , onFistSegment func ()) {
127
155
// subscribe to the outputs and send them into LPMS
128
156
subscriber := trickle .NewTrickleSubscriber (url .String ())
@@ -131,15 +159,24 @@ func startTrickleSubscribe(ctx context.Context, url *url.URL, params aiRequestPa
131
159
params .liveParams .stopPipeline (fmt .Errorf ("error getting pipe for trickle-ffmpeg. url=%s %w" , url , err ))
132
160
return
133
161
}
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
+ }
134
167
ctx = clog .AddVal (ctx , "url" , url .Redacted ())
135
168
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 }}
136
172
137
173
// read segments from trickle subscription
138
174
go func () {
139
175
var err error
140
176
firstSegment := true
141
177
142
178
defer w .Close ()
179
+ defer wMediaMTX .Close ()
143
180
retries := 0
144
181
// we're trying to keep (retryPause x maxRetries) duration to fall within one output GOP length
145
182
const retryPause = 300 * time .Millisecond
@@ -178,7 +215,7 @@ func startTrickleSubscribe(ctx context.Context, url *url.URL, params aiRequestPa
178
215
seq := trickle .GetSeq (segment )
179
216
clog .V (8 ).Infof (ctx , "trickle subscribe read data received seq=%d" , seq )
180
217
181
- n , err := copySegment (segment , w )
218
+ n , err := copySegment (segment , multiWriter )
182
219
if err != nil {
183
220
params .liveParams .stopPipeline (fmt .Errorf ("trickle subscribe error copying: %w" , err ))
184
221
return
@@ -191,43 +228,50 @@ func startTrickleSubscribe(ctx context.Context, url *url.URL, params aiRequestPa
191
228
}
192
229
}()
193
230
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 )
213
233
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" )
227
247
}
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 ))
229
250
}
230
251
}()
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
+ }
231
275
}
232
276
233
277
func copySegment (segment * http.Response , w io.Writer ) (int64 , error ) {
0 commit comments