Skip to content

Commit 44adce9

Browse files
committed
wip
1 parent d069973 commit 44adce9

File tree

22 files changed

+221
-177
lines changed

22 files changed

+221
-177
lines changed

tools/src/bin/generate_from_types.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ enum ApiTypes {
3030
RegisterWebRenderer(smelter_api::WebRendererSpec),
3131
RegisterShader(smelter_api::ShaderSpec),
3232
UpdateOutput(Box<routes::update_output::UpdateOutputRequest>),
33+
UpdateInput(routes::update_input::UpdateInputRequest),
3334
}
3435

3536
pub fn generate_json_schema(check_flag: bool) {

ts/create-smelter-app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
],
1515
"main": "dist/index.js",
1616
"scripts": {
17-
"lint": "eslint src",
17+
"lint": "eslint ./src",
1818
"typecheck": "tsc --noEmit",
1919
"watch": "tsc --watch --preserveWatchOutput",
2020
"build": "tsc",

ts/examples/node-examples/src/utils.ts

Lines changed: 1 addition & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import path from 'path';
2-
import fs, { mkdirp, pathExists, writeFile } from 'fs-extra';
2+
import fs, { mkdirp, pathExists } from 'fs-extra';
33
import type { ChildProcess } from 'child_process';
44
import { spawn as nodeSpawn } from 'child_process';
55
import { promisify } from 'util';
@@ -8,28 +8,6 @@ import fetch from 'node-fetch';
88

99
const pipeline = promisify(Stream.pipeline);
1010

11-
const TMP_SDP_DIR = '/tmp/smelter-examples';
12-
13-
export async function ffplayStartRtpServerAsync(
14-
ip: string,
15-
video_port: number,
16-
audio_port: number | undefined = undefined
17-
): Promise<{ spawn_promise: SpawnPromise }> {
18-
await mkdirp(TMP_SDP_DIR);
19-
let sdpFilePath;
20-
if (audio_port === undefined) {
21-
sdpFilePath = path.join(TMP_SDP_DIR, `video_input_${video_port}.sdp`);
22-
await writeVideoSdpFile(ip, video_port, sdpFilePath);
23-
} else {
24-
sdpFilePath = path.join(TMP_SDP_DIR, `video_audio_input_${video_port}_${audio_port}.sdp`);
25-
await writeVideoAudioSdpFile(ip, video_port, audio_port, sdpFilePath);
26-
}
27-
28-
const promise = spawn('ffplay', ['-protocol_whitelist', 'file,rtp,udp', sdpFilePath]);
29-
await sleep(2000);
30-
return { spawn_promise: promise };
31-
}
32-
3311
export async function ffplayStartRtmpServerAsync(
3412
port: number
3513
): Promise<{ spawn_promise: SpawnPromise }> {
@@ -41,37 +19,6 @@ export async function ffplayStartRtmpServerAsync(
4119
return { spawn_promise: promise };
4220
}
4321

44-
export async function gstReceiveTcpStream(
45-
ip: string,
46-
port: number
47-
): Promise<{ spawn_promise: SpawnPromise }> {
48-
const tcpReceiver = `tcpclientsrc host=${ip} port=${port} ! "application/x-rtp-stream" ! rtpstreamdepay ! queue ! demux.`;
49-
const videoPipe =
50-
'demux.src_96 ! "application/x-rtp,media=video,clock-rate=90000,encoding-name=H264" ! queue ! rtph264depay ! decodebin ! videoconvert ! autovideosink';
51-
const audioPipe =
52-
'demux.src_97 ! "application/x-rtp,media=audio,clock-rate=48000,encoding-name=OPUS" ! queue ! rtpopusdepay ! decodebin ! audioconvert ! autoaudiosink ';
53-
const gstCmd = `gst-launch-1.0 -v rtpptdemux name=demux ${tcpReceiver} ${videoPipe} ${audioPipe}`;
54-
55-
const promise = spawn('bash', ['-c', gstCmd]);
56-
return { spawn_promise: promise };
57-
}
58-
59-
export function ffmpegSendVideoFromMp4(port: number, mp4Path: string): SpawnPromise {
60-
return spawn('ffmpeg', [
61-
'-stream_loop',
62-
'-1',
63-
'-re',
64-
'-i',
65-
mp4Path,
66-
'-an',
67-
'-c:v',
68-
'libx264',
69-
'-f',
70-
'rtp',
71-
`rtp://127.0.0.1:${port}?rtcpport=${port}`,
72-
]);
73-
}
74-
7522
interface SpawnPromise extends Promise<void> {
7623
child: ChildProcess;
7724
}
@@ -96,46 +43,6 @@ function spawn(command: string, args: string[]): SpawnPromise {
9643
}) as SpawnPromise;
9744
}
9845

99-
async function writeVideoAudioSdpFile(
100-
ip: string,
101-
video_port: number,
102-
audio_port: number,
103-
destination: string
104-
): Promise<void> {
105-
await writeFile(
106-
destination,
107-
`
108-
v=0
109-
o=- 0 0 IN IP4 ${ip}
110-
s=No Name
111-
c=IN IP4 ${ip}
112-
m=video ${video_port} RTP/AVP 96
113-
a=rtpmap:96 H264/90000
114-
a=fmtp:96 packetization-mode=1
115-
a=rtcp-mux
116-
m=audio ${audio_port} RTP/AVP 97
117-
a=rtpmap:97 opus/48000/2
118-
a=rtcp-mux
119-
`
120-
);
121-
}
122-
123-
async function writeVideoSdpFile(ip: string, port: number, destination: string): Promise<void> {
124-
await writeFile(
125-
destination,
126-
`
127-
v=0
128-
o=- 0 0 IN IP4 ${ip}
129-
s=No Name
130-
c=IN IP4 ${ip}
131-
m=video ${port} RTP/AVP 96
132-
a=rtpmap:96 H264/90000
133-
a=fmtp:96 packetization-mode=1
134-
a=rtcp-mux
135-
`
136-
);
137-
}
138-
13946
export async function sleep(timeoutMs: number): Promise<void> {
14047
await new Promise<void>(res => {
14148
setTimeout(() => {

ts/examples/vite-example/src/smelter-examples/MultipleOutputs.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ function MultipleOutputs() {
6060
);
6161
}
6262

63-
function SceneTile(props: { state?: 'ready' | 'playing' | 'finished'; inputId: string }) {
63+
function SceneTile(props: {
64+
state?: 'ready' | 'playing' | 'paused' | 'finished';
65+
inputId: string;
66+
}) {
6467
if (props.state === 'playing') {
6568
return (
6669
<View>

ts/examples/vite-example/src/smelter-examples/playground/Scene.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ function volumeForId(store: OutputStore, id: string): number {
4848

4949
type InputTileProps = {
5050
inputId: string;
51-
videoState?: 'ready' | 'playing' | 'finished';
52-
audioState?: 'ready' | 'playing' | 'finished';
51+
videoState?: 'ready' | 'playing' | 'paused' | 'finished';
52+
audioState?: 'ready' | 'playing' | 'paused' | 'finished';
5353
offsetMs?: number | null;
5454
videoDurationMs?: number;
5555
audioDurationMs?: number;

ts/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "module",
66
"scripts": {
77
"lint": "pnpm -r --no-bail run lint",
8-
"lint:fix": "pnpm -r run lint -- --fix",
8+
"lint:fix": "pnpm -r run lint --fix",
99
"build": "pnpm -r run build",
1010
"build:web-wasm": "pnpm -C smelter-browser-render run build-wasm && pnpm -C smelter run build && pnpm -C smelter-core run build && pnpm -C smelter-browser-render run build && pnpm -C smelter-web-wasm run build",
1111
"build:web-client": "pnpm -C smelter run build && pnpm -C smelter-core run build && pnpm -C smelter-web-client run build",

ts/smelter-core/src/api.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,15 @@ export type MultipartRequest = {
2121
};
2222

2323
export type RegisterInputResponse = {
24-
endpoint_route?: string;
25-
bearer_token?: string;
24+
// Common options (but MP4 only for now)
25+
2626
video_duration_ms?: number;
2727
audio_duration_ms?: number;
28+
29+
// WHIP specific
30+
31+
endpoint_route?: string;
32+
bearer_token?: string;
2833
};
2934

3035
export type RegisterOutputResponse = {
@@ -79,6 +84,17 @@ export class ApiClient {
7984
});
8085
}
8186

87+
public async updateInput(
88+
inputId: InputRef,
89+
request: Api.UpdateInputRequest
90+
): Promise<RegisterInputResponse> {
91+
return this.serverManager.sendRequest({
92+
method: 'POST',
93+
route: `/api/input/${encodeURIComponent(inputRefIntoRawId(inputId))}/update`,
94+
body: request,
95+
});
96+
}
97+
8298
public async unregisterInput(
8399
inputId: InputRef,
84100
body: { schedule_time_ms?: number }

ts/smelter-core/src/event.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export function parseEvent(event: any, logger: Logger): SmelterEvent | null {
1515
SmelterEventType.AUDIO_INPUT_DELIVERED,
1616
SmelterEventType.VIDEO_INPUT_PLAYING,
1717
SmelterEventType.AUDIO_INPUT_PLAYING,
18+
SmelterEventType.VIDEO_INPUT_PAUSED,
19+
SmelterEventType.AUDIO_INPUT_PAUSED,
1820
SmelterEventType.VIDEO_INPUT_EOS,
1921
SmelterEventType.AUDIO_INPUT_EOS,
2022
].includes(event.type)

ts/smelter-core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export { OfflineSmelter } from './offline/compositor';
88
export { SmelterManager, SetupInstanceOptions } from './smelterManager';
99
export { Logger, LoggerLevel } from './logger';
1010
export { StateGuard } from './utils';
11+
export { InputHandle, WhipInputHandle, Mp4InputHandle } from './inputHandle';

ts/smelter-core/src/inputHandle.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { ApiClient, RegisterInputResponse } from './api';
2+
import type { InputRef, RegisterInput } from './api/input';
3+
4+
export function newInputHandle(
5+
inputRef: InputRef,
6+
api: ApiClient,
7+
response: RegisterInputResponse,
8+
kind: RegisterInput['type']
9+
) {
10+
if (kind == 'whip_server') {
11+
return new WhipInputHandle(inputRef, api, response);
12+
} else if (kind == 'mp4') {
13+
return new Mp4InputHandle(inputRef, api, response);
14+
} else {
15+
return new InputHandle(inputRef, api, response);
16+
}
17+
}
18+
19+
export class InputHandle {
20+
protected inputRef: InputRef;
21+
protected api: ApiClient;
22+
protected registerResponse: RegisterInputResponse;
23+
24+
constructor(inputRef: InputRef, api: ApiClient, response: RegisterInputResponse) {
25+
this.inputRef = inputRef;
26+
this.api = api;
27+
this.registerResponse = response;
28+
}
29+
30+
public get videoDurationMs(): number | undefined {
31+
return this.registerResponse.video_duration_ms;
32+
}
33+
34+
public get audioDurationMs(): number | undefined {
35+
return this.registerResponse.audio_duration_ms;
36+
}
37+
38+
public async pause(): Promise<void> {
39+
await this.api.updateInput(this.inputRef, { pause: true });
40+
}
41+
42+
public async resume(): Promise<void> {
43+
await this.api.updateInput(this.inputRef, { pause: false });
44+
}
45+
}
46+
47+
export class Mp4InputHandle extends InputHandle {
48+
public async seek(seekMs: number): Promise<void> {
49+
await this.api.updateInput(this.inputRef, { seek_ms: seekMs });
50+
}
51+
}
52+
53+
export class WhipInputHandle extends InputHandle {
54+
public get endpointRoute(): string | undefined {
55+
return this.registerResponse.endpoint_route;
56+
}
57+
58+
public get bearerToken(): string | undefined {
59+
return this.registerResponse.bearer_token;
60+
}
61+
}

0 commit comments

Comments
 (0)