mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-08 08:39:59 +00:00
223 lines
7.3 KiB
TypeScript
223 lines
7.3 KiB
TypeScript
import options from "../../lib/options";
|
|
import { Logger } from "../../lib/logger";
|
|
|
|
import type { ReceiverDevice } from "../../types";
|
|
|
|
import type { ReceiverAvailability } from "../sdk/enums";
|
|
import type Session from "../sdk/Session";
|
|
|
|
import cast, { ensureInit } from "../export";
|
|
|
|
const logger = new Logger("fx_cast [mirroring sender]");
|
|
|
|
const NS_FX_CAST = "urn:x-cast:fx_cast";
|
|
|
|
type MirroringAppMessage =
|
|
| { subject: "peerConnectionOffer"; data: RTCSessionDescriptionInit }
|
|
| { subject: "peerConnectionAnswer"; data: RTCSessionDescriptionInit }
|
|
| { subject: "iceCandidate"; data: RTCIceCandidateInit }
|
|
| { subject: "close" };
|
|
|
|
interface MirroringSenderOpts {
|
|
receiverDevice: ReceiverDevice;
|
|
onSessionCreated: () => void;
|
|
onMirroringConnected: () => void;
|
|
onMirroringStopped: () => void;
|
|
}
|
|
|
|
export default class MirroringSender {
|
|
private receiverDevice: ReceiverDevice;
|
|
private sessionCreatedCallback: () => void;
|
|
private mirroringConnectedCallback: () => void;
|
|
private mirroringStoppedCallback: () => void;
|
|
|
|
private session?: Session;
|
|
private wasSessionRequested = false;
|
|
|
|
private peerConnection: Optional<RTCPeerConnection>;
|
|
|
|
// Stream opts
|
|
private streamMaxFrameRate = 1;
|
|
private streamMaxBitRate = 1;
|
|
private streamDownscaleFactor = 1;
|
|
private streamUseMaxResolution = false;
|
|
private streamMaxResolution: { width?: number; height?: number } = {};
|
|
|
|
constructor(opts: MirroringSenderOpts) {
|
|
this.receiverDevice = opts.receiverDevice;
|
|
this.sessionCreatedCallback = opts.onSessionCreated;
|
|
this.mirroringConnectedCallback = opts.onMirroringConnected;
|
|
this.mirroringStoppedCallback = opts.onMirroringStopped;
|
|
|
|
this.init();
|
|
}
|
|
|
|
private async init() {
|
|
try {
|
|
await ensureInit({ receiverDevice: this.receiverDevice });
|
|
} catch (err) {
|
|
logger.error("Failed to initialize cast API", err);
|
|
}
|
|
|
|
const {
|
|
mirroringAppId,
|
|
mirroringStreamMaxFrameRate,
|
|
mirroringStreamMaxBitRate,
|
|
mirroringStreamDownscaleFactor,
|
|
mirroringStreamUseMaxResolution,
|
|
mirroringStreamMaxResolution
|
|
} = await options.getAll();
|
|
|
|
this.streamMaxFrameRate = mirroringStreamMaxFrameRate;
|
|
this.streamMaxBitRate = mirroringStreamMaxBitRate;
|
|
this.streamDownscaleFactor = mirroringStreamDownscaleFactor;
|
|
this.streamUseMaxResolution = mirroringStreamUseMaxResolution;
|
|
this.streamMaxResolution = mirroringStreamMaxResolution;
|
|
|
|
const sessionRequest = new cast.SessionRequest(mirroringAppId);
|
|
|
|
const apiConfig = new cast.ApiConfig(
|
|
sessionRequest,
|
|
this.sessionListener,
|
|
this.receiverListener
|
|
);
|
|
|
|
cast.initialize(apiConfig);
|
|
}
|
|
|
|
private sessionListener() {
|
|
// Unused
|
|
}
|
|
private receiverListener = (availability: ReceiverAvailability) => {
|
|
if (this.wasSessionRequested) return;
|
|
this.wasSessionRequested = true;
|
|
|
|
if (availability === cast.ReceiverAvailability.AVAILABLE) {
|
|
cast.requestSession(
|
|
session => {
|
|
this.session = session;
|
|
this.sessionCreatedCallback();
|
|
},
|
|
err => {
|
|
logger.error("Session request failed", err);
|
|
}
|
|
);
|
|
}
|
|
};
|
|
|
|
private sendMirroringAppMessage(message: MirroringAppMessage) {
|
|
if (!this.session) return;
|
|
this.session.sendMessage(NS_FX_CAST, message);
|
|
}
|
|
|
|
stop() {
|
|
this.peerConnection?.close();
|
|
this.session?.stop();
|
|
|
|
this.mirroringStoppedCallback();
|
|
}
|
|
|
|
async createMirroringConnection(stream: MediaStream) {
|
|
const pc = new RTCPeerConnection();
|
|
this.peerConnection = pc;
|
|
|
|
this.session?.addMessageListener(NS_FX_CAST, async (_ns, message) => {
|
|
const parsedMessage = JSON.parse(message) as MirroringAppMessage;
|
|
switch (parsedMessage.subject) {
|
|
case "peerConnectionAnswer":
|
|
pc.setRemoteDescription(parsedMessage.data);
|
|
break;
|
|
case "iceCandidate":
|
|
pc.addIceCandidate(parsedMessage.data);
|
|
break;
|
|
}
|
|
});
|
|
|
|
pc.addEventListener("negotiationneeded", async () => {
|
|
const offer = await pc.createOffer();
|
|
await pc.setLocalDescription(offer);
|
|
|
|
this.sendMirroringAppMessage({
|
|
subject: "peerConnectionOffer",
|
|
data: offer
|
|
});
|
|
});
|
|
|
|
pc.addEventListener("icecandidate", ev => {
|
|
if (!ev.candidate) return;
|
|
this.sendMirroringAppMessage({
|
|
subject: "iceCandidate",
|
|
data: ev.candidate
|
|
});
|
|
});
|
|
|
|
// Connection listener
|
|
pc.addEventListener("iceconnectionstatechange", async () => {
|
|
if (pc.iceConnectionState !== "connected") {
|
|
return;
|
|
}
|
|
|
|
this.mirroringConnectedCallback();
|
|
applyParameters();
|
|
});
|
|
|
|
/** Applies stream encoding parameters. */
|
|
const applyParameters = async () => {
|
|
// Set stream encoding parameters
|
|
const [sender] = pc.getSenders();
|
|
const params = sender.getParameters();
|
|
if (!params.encodings) {
|
|
params.encodings = [{}];
|
|
}
|
|
|
|
const [encoding] = params.encodings;
|
|
|
|
if (!(encoding as any).maxFramerate) {
|
|
(encoding as any).maxFramerate = this.streamMaxFrameRate;
|
|
}
|
|
if (!encoding.maxBitrate) {
|
|
encoding.maxBitrate = this.streamMaxBitRate;
|
|
}
|
|
|
|
encoding.scaleResolutionDownBy = this.streamDownscaleFactor;
|
|
|
|
// Handle limiting stream resolution
|
|
if (this.streamUseMaxResolution) {
|
|
const { width: trackWidth, height: trackHeight } =
|
|
sender.track?.getSettings() ?? {};
|
|
|
|
// Calculate downscale ratios for width/height
|
|
let widthRatio = 1;
|
|
let heightRatio = 1;
|
|
if (trackWidth && this.streamMaxResolution.width) {
|
|
widthRatio = trackWidth / this.streamMaxResolution.width;
|
|
}
|
|
if (trackHeight && this.streamMaxResolution.height) {
|
|
heightRatio = trackHeight / this.streamMaxResolution.height;
|
|
}
|
|
|
|
// Use the largest ratio to ensure below resolution limit
|
|
const downscaleRatio = Math.max(1, widthRatio, heightRatio);
|
|
|
|
// Multiply existing downscale
|
|
encoding.scaleResolutionDownBy *= downscaleRatio;
|
|
}
|
|
|
|
await sender.setParameters(params);
|
|
};
|
|
|
|
const [track] = stream.getVideoTracks();
|
|
pc.addTrack(track, stream);
|
|
track.addEventListener("ended", () => this.stop());
|
|
|
|
/**
|
|
* Use a video element to get stream resize events and update
|
|
* scaling parameters.
|
|
*/
|
|
const video = document.createElement("video");
|
|
video.srcObject = stream;
|
|
video.addEventListener("resize", () => applyParameters());
|
|
video.play();
|
|
}
|
|
}
|