tangled
alpha
login
or
join now
stream.place
/
streamplace
74
fork
atom
Live video on the AT Protocol
74
fork
atom
overview
issues
1
pulls
pipelines
rtmp_push: translations, bugfixes, wire it up
Eli Mallon
2 months ago
14a5fc5e
17436c30
+246
-102
10 changed files
expand all
collapse all
unified
split
Makefile
js
app
components
live-dashboard
bento-grid.tsx
livestream-panel.tsx
settings
multistream-manager.tsx
streaming-category-settings.tsx
webhook-manager.tsx
src
router.tsx
components
locales
en-US
settings.ftl
pkg
director
stream_session.go
media
rtmp_push.go
-1
Makefile
···
177
177
-D "gst-plugins-good:videobox=enabled" \
178
178
-D "gst-plugins-good:jpeg=enabled" \
179
179
-D "gst-plugins-good:audioparsers=enabled" \
180
180
-
-D "gst-plugins-good:flv=enabled" \
181
180
-D "gst-plugins-bad:videoparsers=enabled" \
182
181
-D "gst-plugins-bad:mpegtsmux=enabled" \
183
182
-D "gst-plugins-bad:mpegtsdemux=enabled" \
+7
js/app/components/live-dashboard/bento-grid.tsx
···
1
1
import { useNavigation } from "@react-navigation/native";
2
2
import {
3
3
+
borders,
3
4
Button,
4
5
Dashboard,
5
6
useLivestreamStore,
···
20
21
import { useSafeAreaInsets } from "react-native-safe-area-context";
21
22
import { useEmojiData } from "utils/emoji";
22
23
import LivestreamPanel from "./livestream-panel";
24
24
+
import MultistreamStatus from "./multistream-status";
23
25
import StreamMonitor from "./stream-monitor";
24
26
25
27
const { flex, p, gap, layout, bg } = zero;
···
192
194
{ maxWidth: isWeb ? 600 : "100%" },
193
195
]}
194
196
>
197
197
+
<View
198
198
+
style={[borders.top.width.thin, borders.top.color.neutral[700]]}
199
199
+
>
200
200
+
<MultistreamStatus />
201
201
+
</View>
195
202
<Dashboard.ChatPanel
196
203
isLive={isLive}
197
204
isConnected={isConnected}
-4
js/app/components/live-dashboard/livestream-panel.tsx
···
28
28
import { useUserProfile } from "store/hooks";
29
29
import { useCaptureVideoFrame } from "../../hooks/useCaptureVideoFrame";
30
30
import { useLiveUser } from "../../hooks/useLiveUser";
31
31
-
import MultistreamStatus from "./multistream-status";
32
31
33
32
const { flex, p, px, py, gap, layout, bg, borders, text, r, w, typography } =
34
33
zero;
···
593
592
</Button>
594
593
</View>
595
594
)}
596
596
-
</View>
597
597
-
<View style={[borders.top.width.thin, borders.top.color.neutral[700]]}>
598
598
-
<MultistreamStatus />
599
595
</View>
600
596
</Wrapper>
601
597
</>
+107
-91
js/app/components/settings/multistream-manager.tsx
···
4
4
DialogFooter,
5
5
Input,
6
6
Text,
7
7
+
TFunction,
8
8
+
useTranslation,
9
9
+
zero,
7
10
} from "@streamplace/components";
8
8
-
import { ThemeProvider } from "@streamplace/components/src/lib/theme/theme";
9
11
import { usePDSAgent } from "@streamplace/components/src/streamplace-store/xrpc";
10
10
-
import {
11
11
-
flex,
12
12
-
gap,
13
13
-
layout,
14
14
-
mb,
15
15
-
mt,
16
16
-
mx,
17
17
-
text,
18
18
-
w,
19
19
-
} from "@streamplace/components/src/ui";
12
12
+
import { gap, layout, mb, mt, text, w } from "@streamplace/components/src/ui";
20
13
import Loading from "components/loading/loading";
21
14
import { Plus, RefreshCw } from "lucide-react-native";
22
15
import { useEffect, useState } from "react";
···
33
26
record: PlaceStreamMultistreamTarget.Record;
34
27
}
35
28
36
36
-
const mulistreamTitle = (target?: MultistreamTargetViewHydrated) => {
29
29
+
const multistreamTitle = (
30
30
+
target: MultistreamTargetViewHydrated | undefined,
31
31
+
t: TFunction,
32
32
+
) => {
37
33
if (!target) {
38
38
-
return "Untitled Target";
34
34
+
return t("untitled-multistream-target");
39
35
}
40
36
if (target.record.name) {
41
37
return target.record.name;
···
45
41
const u = new URL(target.record.url);
46
42
return u.host;
47
43
} catch (error) {
48
48
-
return "Untitled Target";
44
44
+
return t("untitled-multistream-target");
49
45
}
50
46
}
51
51
-
return "Untitled Target";
47
47
+
return t("untitled-multistream-target");
52
48
};
53
49
54
50
export default function MultistreamManager() {
51
51
+
const { theme } = zero.useTheme();
52
52
+
const { t } = useTranslation("settings");
55
53
const agent = usePDSAgent();
56
54
const [loading, setLoading] = useState(true);
57
55
const [targets, setTargets] = useState<
···
86
84
setTargets(targetViews.data.targets as MultistreamTargetViewHydrated[]);
87
85
} catch (error) {
88
86
console.error("Failed to load multistream targets:", error);
89
89
-
Alert.alert(
90
90
-
"Error",
91
91
-
"Failed to load multistream targets. Please try again.",
92
92
-
);
87
87
+
Alert.alert("Error", t("failed-load-multistream-targets"));
93
88
} finally {
94
89
setLoading(false);
95
90
}
···
158
153
await loadMultistreamTargets();
159
154
} catch (error) {
160
155
console.error("Failed to toggle multistream target:", error);
161
161
-
Alert.alert(
162
162
-
"Error",
163
163
-
"Failed to toggle multistream target. Please try again.",
164
164
-
);
156
156
+
Alert.alert("Error", t("failed-toggle-multistream-target"));
165
157
} finally {
166
158
setTogglingTargets((prev) => {
167
159
const newSet = new Set(prev);
···
209
201
};
210
202
211
203
return (
212
212
-
<ThemeProvider>
213
213
-
<View style={[flex.values[1]]}>
214
214
-
<ScrollView style={[flex.values[1]]}>
215
215
-
<View style={[{ maxWidth: 800 }, mx.auto]}>
204
204
+
<>
205
205
+
<ScrollView>
206
206
+
<View style={[zero.layout.flex.align.center, zero.px[2], zero.py[2]]}>
207
207
+
<View style={{ maxWidth: 800, width: "100%" }}>
216
208
{/* Header */}
217
209
<View style={[mb[6]]}>
218
210
<Text style={[mb[2], { fontSize: 24, fontWeight: "700" }]}>
219
219
-
Multistream Targets
211
211
+
{t("multistream-targets")}
220
212
</Text>
221
213
<Text style={[text.gray[400], mb[4], { fontSize: 14 }]}>
222
222
-
Automatically push your Streamplace livestreams to other
223
223
-
streaming services like Twitch or YouTube.
214
214
+
{t("multistream-description")}
224
215
</Text>
225
225
-
<View style={[layout.flex.row, gap.all[3]]}>
226
226
-
<Button onPress={handleCreate} size="sm" leftIcon={<Plus />}>
227
227
-
<Text>Create Multistream Target</Text>
216
216
+
217
217
+
<View
218
218
+
style={[
219
219
+
layout.flex.row,
220
220
+
layout.flex.justify.start,
221
221
+
gap.all[3],
222
222
+
w.percent[100],
223
223
+
mt[2],
224
224
+
]}
225
225
+
>
226
226
+
<Button
227
227
+
onPress={handleCreate}
228
228
+
size="pill"
229
229
+
width="min"
230
230
+
leftIcon={<Plus color={theme.colors.text} />}
231
231
+
>
232
232
+
<Text>{t("create-multistream-target")}</Text>
228
233
</Button>
229
234
230
235
<Button
231
236
onPress={loadMultistreamTargets}
232
237
disabled={loading}
233
233
-
leftIcon={<RefreshCw />}
234
234
-
size="sm"
238
238
+
leftIcon={<RefreshCw color={theme.colors.text} />}
239
239
+
size="pill"
240
240
+
width="min"
241
241
+
variant="secondary"
235
242
>
236
236
-
<Text>Refresh</Text>
243
243
+
<Text>{t("refresh")}</Text>
237
244
</Button>
238
245
</View>
239
246
</View>
···
245
252
) : targets === null ? (
246
253
<View style={[layout.flex.center, mt[8]]}>
247
254
<Text style={[text.gray[600]]}>
248
248
-
Failed to load multistream targets
255
255
+
{t("failed-load-multistream-targets")}
249
256
</Text>
250
257
</View>
251
258
) : targets.length === 0 ? (
252
259
<View style={[layout.flex.center, mt[8]]}>
253
260
<Text style={[text.gray[600], mb[4], { fontSize: 16 }]}>
254
254
-
No targets yet!
261
261
+
{t("no-multistream-targets-yet")}
255
262
</Text>
256
263
</View>
257
264
) : (
258
265
<>
259
266
<View style={[mb[4]]}>
260
267
<Text style={[text.gray[600], { fontSize: 14 }]}>
261
261
-
{targets.length} target{targets.length !== 1 && "s"}
268
268
+
{t("multistream-targets-count", { count: targets.length })}
262
269
</Text>
263
270
</View>
264
271
{targets.map((target) => (
···
280
287
))}
281
288
</>
282
289
)}
283
283
-
</ScrollView>
284
284
-
<MultistreamTargetForm
285
285
-
target={editingTarget}
286
286
-
isVisible={showForm}
287
287
-
onClose={() => {
288
288
-
setShowForm(false);
289
289
-
}}
290
290
-
onSubmit={(record: PlaceStreamMultistreamTarget.Record) => {
291
291
-
if (editingTarget) {
292
292
-
editMultistreamTarget(editingTarget.uri, record);
293
293
-
} else {
294
294
-
createMultistreamTarget(record);
295
295
-
}
296
296
-
}}
297
297
-
isLoading={formLoading}
298
298
-
formError={formError}
299
299
-
/>
300
300
-
301
301
-
<MultistreamTargetDeleteDialog
302
302
-
target={deleteDialog.target || undefined}
303
303
-
isVisible={deleteDialog.isVisible}
304
304
-
onClose={() =>
305
305
-
setDeleteDialog({
306
306
-
isVisible: false,
307
307
-
target: null,
308
308
-
isLoading: false,
309
309
-
})
290
290
+
</View>
291
291
+
</ScrollView>
292
292
+
<MultistreamTargetForm
293
293
+
target={editingTarget}
294
294
+
isVisible={showForm}
295
295
+
onClose={() => {
296
296
+
setShowForm(false);
297
297
+
}}
298
298
+
onSubmit={(record: PlaceStreamMultistreamTarget.Record) => {
299
299
+
if (editingTarget) {
300
300
+
editMultistreamTarget(editingTarget.uri, record);
301
301
+
} else {
302
302
+
createMultistreamTarget(record);
310
303
}
311
311
-
onSubmit={() =>
312
312
-
deleteDialog.target &&
313
313
-
deleteMultistreamTarget(deleteDialog.target.uri)
314
314
-
}
315
315
-
isLoading={deleteDialog.isLoading}
316
316
-
formError={formError}
317
317
-
/>
318
318
-
</View>
319
319
-
</ThemeProvider>
304
304
+
}}
305
305
+
isLoading={formLoading}
306
306
+
formError={formError}
307
307
+
/>
308
308
+
309
309
+
<MultistreamTargetDeleteDialog
310
310
+
target={deleteDialog.target || undefined}
311
311
+
isVisible={deleteDialog.isVisible}
312
312
+
onClose={() =>
313
313
+
setDeleteDialog({
314
314
+
isVisible: false,
315
315
+
target: null,
316
316
+
isLoading: false,
317
317
+
})
318
318
+
}
319
319
+
onSubmit={() =>
320
320
+
deleteDialog.target &&
321
321
+
deleteMultistreamTarget(deleteDialog.target.uri)
322
322
+
}
323
323
+
isLoading={deleteDialog.isLoading}
324
324
+
formError={formError}
325
325
+
/>
326
326
+
</>
320
327
);
321
328
}
322
329
···
335
342
isDeleting: boolean;
336
343
isToggling: boolean;
337
344
}) {
345
345
+
const { t } = useTranslation("settings");
338
346
// Determine latest event status for footer
339
347
const getStatusInfo = () => {
340
348
if (target.latestEvent) {
341
349
return (
342
350
<View style={[layout.flex.row, gap.all[4]]}>
343
351
<Text style={[text.gray[400], { fontSize: 11 }]}>
344
344
-
Status: {target.latestEvent.status}
352
352
+
{t("status")}: {target.latestEvent.status}
345
353
</Text>
346
354
<Text style={[text.gray[400], { fontSize: 11 }]}>
347
355
{timeAgo(new Date(target.latestEvent.createdAt))}
···
354
362
355
363
return (
356
364
<SettingsListItem
357
357
-
title={mulistreamTitle(target)}
365
365
+
title={multistreamTitle(target, t)}
358
366
url={target.record.url}
359
367
active={target.record.active}
360
368
isDeleting={isDeleting}
361
369
isToggling={isToggling}
362
370
footer={{
363
363
-
left: `Created ${timeAgo(new Date(target.record.createdAt))}`,
371
371
+
left: `${t("created")} ${timeAgo(new Date(target.record.createdAt))}`,
364
372
right: getStatusInfo(),
365
373
}}
366
374
onEdit={() => onEdit(target)}
···
385
393
isLoading: boolean;
386
394
formError: string;
387
395
}) {
396
396
+
const { t } = useTranslation("settings");
388
397
const [formData, setFormData] = useState<PlaceStreamMultistreamTarget.Record>(
389
398
{
390
399
$type: "place.stream.multistream.target",
···
443
452
<Dialog
444
453
open={isVisible}
445
454
onOpenChange={(open) => !open && onClose()}
446
446
-
title={target ? "Edit Target" : "Create Target"}
455
455
+
title={
456
456
+
target ? t("multistream-edit-target") : t("multistream-create-target")
457
457
+
}
447
458
size="lg"
448
459
dismissible={false}
449
460
>
···
453
464
<Text
454
465
style={[text.gray[300], mb[2], { fontSize: 14, fontWeight: "500" }]}
455
466
>
456
456
-
Name (optional)
467
467
+
{t("rtmp-target-name")} ({t("optional")})
457
468
</Text>
458
469
<Input
459
470
value={formData.name}
460
471
onChangeText={(text) =>
461
472
setFormData((prev) => ({ ...prev, name: text }))
462
473
}
463
463
-
placeholder="My Multistream Target"
474
474
+
placeholder={t("rtmp-target-name-placeholder")}
464
475
/>
465
476
</View>
466
477
···
469
480
<Text
470
481
style={[text.gray[300], mb[2], { fontSize: 14, fontWeight: "500" }]}
471
482
>
472
472
-
Webhook URL *
483
483
+
{t("rtmp-target-url")} *
473
484
</Text>
474
485
<Input
475
486
value={formData.url}
···
497
508
]}
498
509
>
499
510
<Text style={[text.gray[300], { fontSize: 14, fontWeight: "500" }]}>
500
500
-
Active
511
511
+
{t("active")}
501
512
</Text>
502
513
<Switch
503
514
value={formData.active}
···
512
523
</View>
513
524
514
525
<DialogFooter>
515
515
-
<Button variant="secondary" onPress={onClose} disabled={isLoading}>
526
526
+
<Button
527
527
+
variant="secondary"
528
528
+
onPress={onClose}
529
529
+
disabled={isLoading}
530
530
+
width="min"
531
531
+
>
516
532
<Text>Cancel</Text>
517
533
</Button>
518
518
-
<Button onPress={handleSubmit} disabled={isLoading}>
534
534
+
<Button onPress={handleSubmit} disabled={isLoading} width="min">
519
535
<Text>{isLoading ? "Saving..." : target ? "Update" : "Create"}</Text>
520
536
</Button>
521
537
</DialogFooter>
···
538
554
isLoading: boolean;
539
555
formError: string;
540
556
}) => {
557
557
+
const { t } = useTranslation("settings");
541
558
return (
542
559
<Dialog
543
560
open={isVisible}
···
547
564
>
548
565
<View style={[w.percent[100], mb[8], mt[2]]}>
549
566
<Text style={[{ fontSize: 24 }]}>
550
550
-
Are you sure you want to delete "{mulistreamTitle(target)}"?
567
567
+
{t("multistream-delete-target-confirmation", {
568
568
+
target: multistreamTitle(target, t),
569
569
+
})}
551
570
</Text>
552
571
<Text
553
572
style={[text.gray[400], mt[4], { fontSize: 18, fontWeight: "700" }]}
554
573
>
555
555
-
This action cannot be undone.
556
556
-
</Text>
557
557
-
<Text style={[text.gray[400], { fontSize: 18, fontWeight: "700" }]}>
558
558
-
The webhook will no longer receive events.
574
574
+
{t("this-action-cannot-be-undone")}
559
575
</Text>
560
576
</View>
561
577
···
573
589
</Button>
574
590
<Button variant="destructive" onPress={onSubmit} disabled={isLoading}>
575
591
<Text style={[text.white, { fontSize: 14, fontWeight: "500" }]}>
576
576
-
{isLoading ? "Deleting..." : "Delete"}
592
592
+
{isLoading ? t("deleting") : t("delete")}
577
593
</Text>
578
594
</Button>
579
595
</View>
+7
-1
js/app/components/settings/streaming-category-settings.tsx
···
5
5
View,
6
6
zero,
7
7
} from "@streamplace/components";
8
8
-
import { Heart, Key, Webhook } from "lucide-react-native";
8
8
+
import { Globe, Heart, Key, Webhook } from "lucide-react-native";
9
9
import { useTranslation } from "react-i18next";
10
10
import { ScrollView } from "react-native";
11
11
import { SettingsNavigationItem } from "./components/settings-navigation-item";
···
34
34
title={t("webhooks")}
35
35
screen="WebhooksSettings"
36
36
icon={Webhook}
37
37
+
/>
38
38
+
<MenuSeparator />
39
39
+
<SettingsNavigationItem
40
40
+
title={t("multistream")}
41
41
+
screen="MultistreamCategory"
42
42
+
icon={Globe}
37
43
/>
38
44
</MenuGroup>
39
45
</MenuContainer>
+7
-2
js/app/components/settings/webhook-manager.tsx
···
594
594
</View>
595
595
596
596
<DialogFooter>
597
597
-
<Button variant="secondary" onPress={onClose} disabled={isLoading}>
597
597
+
<Button
598
598
+
width="min"
599
599
+
variant="secondary"
600
600
+
onPress={onClose}
601
601
+
disabled={isLoading}
602
602
+
>
598
603
<Text>{t("cancel")}</Text>
599
604
</Button>
600
600
-
<Button onPress={handleSubmit} disabled={isLoading}>
605
605
+
<Button width="min" onPress={handleSubmit} disabled={isLoading}>
601
606
<Text>
602
607
{isLoading ? t("saving") : webhook ? t("update") : t("create")}
603
608
</Text>
+10
js/app/src/router.tsx
···
73
73
74
74
import { useUrl } from "@streamplace/components";
75
75
import { LanguagesCategorySettings } from "components/settings/languages-category-settings";
76
76
+
import MultistreamManager from "components/settings/multistream-manager";
76
77
import RecommendationsManager from "components/settings/recommendations-manager";
77
78
import Constants from "expo-constants";
78
79
import { useBlueskyNotifications } from "hooks/useBlueskyNotifications";
···
123
124
LanguagesCategory: undefined;
124
125
DeveloperSettings: undefined;
125
126
KeyManagement: undefined;
127
127
+
MultistreamCategory: undefined;
126
128
};
127
129
128
130
type RootStackParamList = {
···
178
180
DanmuCategory: "settings/danmu",
179
181
AdvancedCategory: "settings/advanced",
180
182
DeveloperSettings: "settings/developer",
183
183
+
MultistreamCategory: "settings/streaming/multistream",
184
184
+
KeyManagement: "settings/streaming/key-management",
185
185
+
LanguagesCategory: "settings/languages",
181
186
},
182
187
},
183
188
KeyManagement: "key-management",
···
801
806
name="KeyManagement"
802
807
component={KeyManager}
803
808
options={{ headerTitle: "Key Manager", title: "Key Manager" }}
809
809
+
/>
810
810
+
<Stack.Screen
811
811
+
name="MultistreamCategory"
812
812
+
component={MultistreamManager}
813
813
+
options={{ headerTitle: "Multistream", title: "Multistream" }}
804
814
/>
805
815
</Stack.Navigator>
806
816
);
+26
js/components/locales/en-US/settings.ftl
···
62
62
sign-in = Sign In
63
63
update = Update
64
64
log-out = Log out
65
65
+
optional = optional
65
66
66
67
## Account Settings
67
68
account-greeting = Hey, @{ $handle }.
···
117
118
events-chat = Chat Events
118
119
untitled-webhook = Untitled Webhook
119
120
inactive = Inactive
121
121
+
active = Active
122
122
+
123
123
+
## Multistreaming
124
124
+
multistreaming = Multistreaming
125
125
+
multistream-targets = Multistream Targets
126
126
+
multistream-description = Automatically push your Streamplace livestreams to other streaming services like Twitch or YouTube.
127
127
+
create-multistream-target = Create Multistream Target
128
128
+
untitled-multistream-target = Untitled Target
129
129
+
failed-load-multistream-targets = Failed to load multistream targets. Please try again.
130
130
+
failed-toggle-multistream-target = Failed to toggle multistream target. Please try again.
131
131
+
failed-delete-multistream-target = Failed to delete multistream target. Please try again.
132
132
+
no-multistream-targets-yet = No targets yet!
133
133
+
multistream-targets-count = { $count ->
134
134
+
[one] { $count } target
135
135
+
*[other] { $count } targets
136
136
+
}
137
137
+
multistream-delete-target-confirmation = Are you sure you want to delete "{ $target }"?
138
138
+
this-action-cannot-be-undone = This action cannot be undone.
139
139
+
rtmp-target-name = RTMP Target
140
140
+
rtmp-target-url = RTMP URL
141
141
+
rtmp-target-name-placeholder = My Multistream Target
142
142
+
multistream-create-target = Create Target
143
143
+
multistream-edit-target = Edit Target
144
144
+
created = created
145
145
+
status = status
120
146
121
147
## Debug Recording
122
148
debug-recording = Debug Recording
+82
pkg/director/stream_session.go
···
109
109
110
110
close(ss.started)
111
111
112
112
+
ss.Go(ctx, func() error {
113
113
+
return ss.HandleMultistreamTargets(ctx)
114
114
+
})
115
115
+
112
116
for {
113
117
select {
114
118
case <-ss.segmentChan:
···
679
683
680
684
return client, nil
681
685
}
686
686
+
687
687
+
type runningMultistream struct {
688
688
+
cancel func()
689
689
+
uri string
690
690
+
}
691
691
+
692
692
+
// we're making an attempt here not to log (sensitive) stream keys, so we're
693
693
+
// referencing by atproto URI
694
694
+
func (ss *StreamSession) HandleMultistreamTargets(ctx context.Context) error {
695
695
+
ctx = log.WithLogValues(ctx, "system", "multistreaming")
696
696
+
isTrue := true
697
697
+
// {target.Uri}:{rec.Url} -> runningMultistream
698
698
+
// no concurrency issues, it's only used from this one loop
699
699
+
running := map[string]*runningMultistream{}
700
700
+
for {
701
701
+
targets, err := ss.statefulDB.ListMultistreamTargets(ss.repoDID, 100, 0, &isTrue)
702
702
+
if err != nil {
703
703
+
return fmt.Errorf("failed to list multistream targets: %w", err)
704
704
+
}
705
705
+
currentRunning := map[string]bool{}
706
706
+
for _, targetView := range targets {
707
707
+
rec, ok := targetView.Record.Val.(*streamplace.MultistreamTarget)
708
708
+
if !ok {
709
709
+
log.Error(ctx, "failed to convert multistream target to streamplace multistream target", "uri", targetView.Uri)
710
710
+
continue
711
711
+
}
712
712
+
key := fmt.Sprintf("%s:%s", targetView.Uri, rec.Url)
713
713
+
if running[key] == nil {
714
714
+
childCtx, childCancel := context.WithCancel(ctx)
715
715
+
ss.Go(ctx, func() error {
716
716
+
log.Log(ctx, "starting multistream target", "uri", targetView.Uri)
717
717
+
err := ss.statefulDB.CreateMultistreamEvent(targetView.Uri, "starting multistream target", "pending")
718
718
+
if err != nil {
719
719
+
log.Error(ctx, "failed to create multistream event", "error", err)
720
720
+
}
721
721
+
return ss.StartMultistreamTarget(childCtx, targetView)
722
722
+
})
723
723
+
running[key] = &runningMultistream{
724
724
+
cancel: childCancel,
725
725
+
uri: key,
726
726
+
}
727
727
+
}
728
728
+
currentRunning[key] = true
729
729
+
}
730
730
+
for key := range running {
731
731
+
if !currentRunning[key] {
732
732
+
log.Log(ctx, "stopping multistream target", "uri", running[key].uri)
733
733
+
running[key].cancel()
734
734
+
delete(running, key)
735
735
+
}
736
736
+
}
737
737
+
select {
738
738
+
case <-ctx.Done():
739
739
+
return nil
740
740
+
case <-time.After(time.Second * 5):
741
741
+
continue
742
742
+
}
743
743
+
}
744
744
+
}
745
745
+
746
746
+
func (ss *StreamSession) StartMultistreamTarget(ctx context.Context, targetView *streamplace.MultistreamDefs_TargetView) error {
747
747
+
for {
748
748
+
err := ss.mm.RTMPPush(ctx, ss.repoDID, "source", targetView)
749
749
+
if err != nil {
750
750
+
log.Error(ctx, "failed to push to RTMP server", "error", err)
751
751
+
err := ss.statefulDB.CreateMultistreamEvent(targetView.Uri, err.Error(), "error")
752
752
+
if err != nil {
753
753
+
log.Error(ctx, "failed to create multistream event", "error", err)
754
754
+
}
755
755
+
}
756
756
+
select {
757
757
+
case <-ctx.Done():
758
758
+
return nil
759
759
+
case <-time.After(time.Second * 5):
760
760
+
continue
761
761
+
}
762
762
+
}
763
763
+
}
-3
pkg/media/rtmp_push.go
···
7
7
"io"
8
8
"net"
9
9
"net/url"
10
10
-
"reflect"
11
10
"strings"
12
11
"time"
13
12
···
18
17
"stream.place/streamplace/pkg/streamplace"
19
18
)
20
19
21
21
-
// This function remains in scope for the duration of a single users' playback
22
20
func (mm *MediaManager) RTMPPush(ctx context.Context, user string, rendition string, targetView *streamplace.MultistreamDefs_TargetView) error {
23
21
uu, err := uuid.NewV7()
24
22
if err != nil {
···
90
88
log.Error(ctx, "failed to get rtmp2sink peak-kbps", "prop", prop)
91
89
continue
92
90
}
93
93
-
log.Warn(ctx, "rtmp2sink peak-kbps", "prop", reflect.TypeOf(prop))
94
91
propVal, ok := prop.(*gst.Structure)
95
92
if !ok {
96
93
log.Error(ctx, "failed to convert rtmp2sink peak-kbps", "prop", prop)