feat(media): add phase5 media provider abstraction and stream credentials APIs
This commit is contained in:
96
Backend/media/providers/mock.ts
Normal file
96
Backend/media/providers/mock.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { createHmac } from 'crypto';
|
||||
|
||||
import type {
|
||||
MediaProvider,
|
||||
MediaPublishCredentials,
|
||||
MediaSessionCreateInput,
|
||||
MediaSessionCreateResult,
|
||||
MediaSubscribeCredentials,
|
||||
} from '../types';
|
||||
|
||||
const secret = process.env.BETTER_AUTH_SECRET;
|
||||
|
||||
if (!secret) {
|
||||
throw new Error('BETTER_AUTH_SECRET is required for mock media provider token signing');
|
||||
}
|
||||
|
||||
const DEFAULT_TTL_SECONDS = 60 * 10;
|
||||
|
||||
const signToken = (payload: Record<string, unknown>, ttlSeconds = DEFAULT_TTL_SECONDS): { token: string; expiresInSeconds: number } => {
|
||||
const body = {
|
||||
...payload,
|
||||
exp: Math.floor(Date.now() / 1000) + ttlSeconds,
|
||||
};
|
||||
|
||||
const encoded = Buffer.from(JSON.stringify(body), 'utf8').toString('base64url');
|
||||
const signature = createHmac('sha256', secret).update(encoded).digest('base64url');
|
||||
|
||||
return {
|
||||
token: `${encoded}.${signature}`,
|
||||
expiresInSeconds: ttlSeconds,
|
||||
};
|
||||
};
|
||||
|
||||
const getBaseUrl = (): string => process.env.MEDIA_MOCK_BASE_URL ?? process.env.BETTER_AUTH_URL ?? 'http://localhost:3000';
|
||||
|
||||
export class MockMediaProvider implements MediaProvider {
|
||||
name = 'mock';
|
||||
|
||||
async createSession(input: MediaSessionCreateInput): Promise<MediaSessionCreateResult> {
|
||||
const mediaSessionId = `mock_${input.streamSessionId}`;
|
||||
const baseUrl = getBaseUrl();
|
||||
|
||||
return {
|
||||
provider: this.name,
|
||||
mediaSessionId,
|
||||
publishUrl: `${baseUrl}/media/mock/publish/${mediaSessionId}`,
|
||||
subscribeUrl: `${baseUrl}/media/mock/subscribe/${mediaSessionId}`,
|
||||
};
|
||||
}
|
||||
|
||||
async issuePublishCredentials(input: {
|
||||
mediaSessionId: string;
|
||||
cameraDeviceId: string;
|
||||
ownerUserId: string;
|
||||
}): Promise<MediaPublishCredentials> {
|
||||
const baseUrl = getBaseUrl();
|
||||
const { token, expiresInSeconds } = signToken({
|
||||
typ: 'publish',
|
||||
provider: this.name,
|
||||
mediaSessionId: input.mediaSessionId,
|
||||
cameraDeviceId: input.cameraDeviceId,
|
||||
ownerUserId: input.ownerUserId,
|
||||
});
|
||||
|
||||
return {
|
||||
provider: this.name,
|
||||
mediaSessionId: input.mediaSessionId,
|
||||
publishToken: token,
|
||||
publishUrl: `${baseUrl}/media/mock/publish/${input.mediaSessionId}`,
|
||||
expiresInSeconds,
|
||||
};
|
||||
}
|
||||
|
||||
async issueSubscribeCredentials(input: {
|
||||
mediaSessionId: string;
|
||||
viewerDeviceId: string;
|
||||
ownerUserId: string;
|
||||
}): Promise<MediaSubscribeCredentials> {
|
||||
const baseUrl = getBaseUrl();
|
||||
const { token, expiresInSeconds } = signToken({
|
||||
typ: 'subscribe',
|
||||
provider: this.name,
|
||||
mediaSessionId: input.mediaSessionId,
|
||||
viewerDeviceId: input.viewerDeviceId,
|
||||
ownerUserId: input.ownerUserId,
|
||||
});
|
||||
|
||||
return {
|
||||
provider: this.name,
|
||||
mediaSessionId: input.mediaSessionId,
|
||||
subscribeToken: token,
|
||||
subscribeUrl: `${baseUrl}/media/mock/subscribe/${input.mediaSessionId}`,
|
||||
expiresInSeconds,
|
||||
};
|
||||
}
|
||||
}
|
||||
15
Backend/media/service.ts
Normal file
15
Backend/media/service.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { MockMediaProvider } from './providers/mock';
|
||||
import type { MediaProvider } from './types';
|
||||
|
||||
const providerName = (process.env.MEDIA_PROVIDER ?? 'mock').toLowerCase();
|
||||
|
||||
const createProvider = (): MediaProvider => {
|
||||
switch (providerName) {
|
||||
case 'mock':
|
||||
return new MockMediaProvider();
|
||||
default:
|
||||
throw new Error(`Unsupported MEDIA_PROVIDER: ${providerName}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const mediaProvider = createProvider();
|
||||
44
Backend/media/types.ts
Normal file
44
Backend/media/types.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
export type MediaSessionCreateInput = {
|
||||
streamSessionId: string;
|
||||
ownerUserId: string;
|
||||
cameraDeviceId: string;
|
||||
requesterDeviceId: string;
|
||||
};
|
||||
|
||||
export type MediaPublishCredentials = {
|
||||
provider: string;
|
||||
mediaSessionId: string;
|
||||
publishToken: string;
|
||||
publishUrl: string;
|
||||
expiresInSeconds: number;
|
||||
};
|
||||
|
||||
export type MediaSubscribeCredentials = {
|
||||
provider: string;
|
||||
mediaSessionId: string;
|
||||
subscribeToken: string;
|
||||
subscribeUrl: string;
|
||||
expiresInSeconds: number;
|
||||
};
|
||||
|
||||
export type MediaSessionCreateResult = {
|
||||
provider: string;
|
||||
mediaSessionId: string;
|
||||
publishUrl: string;
|
||||
subscribeUrl: string;
|
||||
};
|
||||
|
||||
export interface MediaProvider {
|
||||
name: string;
|
||||
createSession(input: MediaSessionCreateInput): Promise<MediaSessionCreateResult>;
|
||||
issuePublishCredentials(input: {
|
||||
mediaSessionId: string;
|
||||
cameraDeviceId: string;
|
||||
ownerUserId: string;
|
||||
}): Promise<MediaPublishCredentials>;
|
||||
issueSubscribeCredentials(input: {
|
||||
mediaSessionId: string;
|
||||
viewerDeviceId: string;
|
||||
ownerUserId: string;
|
||||
}): Promise<MediaSubscribeCredentials>;
|
||||
}
|
||||
Reference in New Issue
Block a user