Skip to content

Instantly share code, notes, and snippets.

@iwalton3
Last active March 15, 2023 05:07
Show Gist options
  • Save iwalton3/8f0f4c5caa51b08975e9b61a0a0047bc to your computer and use it in GitHub Desktop.
Save iwalton3/8f0f4c5caa51b08975e9b61a0a0047bc to your computer and use it in GitHub Desktop.
Prototype Jellyfin Media Player Typescript API
interface JMPSettings {
appleremote: {
emulatepht: boolean;
};
audio: {
channels: 'auto' | '2.0' | '5.1,2.0' | '7.1,5.1,2.0';
/**
* auto or device name
*/
device: string;
devicetype: 'basic' | 'spdif' | 'hdmi';
exclusive: boolean;
normalize: boolean;
/**
* requires spdif or hdmi
*/
"passthrough.ac3": boolean;
/**
* requires spdif or hdmi
*/
"passthrough.dts": boolean;
/**
* requires hdmi
*/
"passthrough.dts-hd": boolean;
/**
* requires hdmi
*/
"passthrough.eac3": boolean;
/**
* requires hdmi
*/
"passthrough.truehd": boolean;
};
cec: {
activatesource: boolean;
enable: boolean;
hdmiport: number;
poweroffonstandby: boolean;
suspendonstandby: boolean;
usekeyupdown: boolean;
verbose_logging: boolean;
};
main: {
alwaysOnTop: boolean;
checkForUpdates: boolean;
disablemouse: boolean;
enableInputRepeat: boolean;
enableWindowsMediaIntegration: boolean;
enableWindowsTaskbarIntegration: boolean;
forceAlwaysFS: boolean;
forceExternalWebclient: boolean;
/**
* A specific screen (e.g. HDMI-1) or empty to disable forcing
*/
forceFSScreen: string;
fullscreen: boolean;
hdmi_poweron: boolean;
ignoreSSLErrors: boolean;
layout: 'desktop' | 'tv';
logLevel: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
minimizeOnDefocus: boolean;
sdlEnabled: boolean;
showPowerOptions: boolean;
useOpenGL: boolean;
useSystemVideoCodecs: boolean;
userWebClient: string;
webMode: 'desktop' | 'tv';
};
path: {
startupurl_desktop: string;
startupurl_extension: string;
};
plugins: {
jellyscrub: boolean;
skipintro: boolean;
};
state: {
geometry: {
height: number;
width: number;
x: number;
y: number;
};
lastUsedScreen: string;
maximized: boolean;
};
subtitles: {
/**
* Color in hex format
* foreground,background e.g. #FBF93E,#000000
*/
color: string;
placement: 'left,bottom' | 'right,bottom' | 'center,bottom' | 'left,top' | 'right,top' | 'center,top';
/**
* Default size is 32
*/
size: number;
};
system: {
lircd_enabled: boolean;
smbd_enabled: boolean;
sshd_enabled: boolean;
systemname: string;
};
video: {
allow_transcode_to_hevc: boolean;
always_force_transcode: boolean;
aspect: 'normal' | 'zoom' | 'force_4_3' | 'force_16_9' | 'force_16_9_if_4_3' | 'stretch' | 'noscaling' | 'custom';
"audio_delay.24hz": number;
"audio_delay.25hz": number;
"audio_delay.50hz": number;
"audio_delay.normal": number;
cache: number;
"debug.force_vo": string;
deinterlace: boolean;
force_transcode_4k: boolean;
force_transcode_av1: boolean;
force_transcode_hdr: boolean;
force_transcode_hevc: boolean;
force_transcode_hi10p: boolean;
hardwareDecoding: boolean;
prefer_transcode_to_h265: boolean;
"refreshrate.auto_switch": boolean;
"refreshrate.avoid_25hz_30hz": boolean;
"refreshrate.delay": number;
"sync_mode": 'audio' | 'display-resample' | 'display-adrop';
};
webclient: {}
}
interface JMPSettingOption {
title: string;
value: string | number;
}
interface JMPSettingInfo {
key: string;
options?: JMPSettingOption[];
}
interface JMPInfo {
deviceName: string;
mode: string;
scriptPath: string;
settings: JMPSettings;
settingsDescriptions: {
audio: JMPSettingInfo[];
main: JMPSettingInfo[];
plugins: JMPSettingInfo[];
subtitles: JMPSettingInfo[];
video: JMPSettingInfo[];
};
settingsDescriptionsUpdate: ((section: string, info: JMPSettingInfo[]) => void)[];
settingsUpdate: ((section: string, settings: object) => void)[];
}
export const jmpInfo = globalThis['jmpInfo'] as JMPInfo;
interface DeviceProfile {
Name: string;
MaxStaticBitrate: number;
MusicStreamingTranscodingBitrate: number;
TimelineOffsetSeconds: number;
DirectPlayProfiles: any[];
TranscodingProfiles: any[];
ContainerProfiles: any[];
CodecProfiles: any[];
ResponseProfiles: any[];
SubtitleProfiles: any[];
}
interface NativeShell {
openUrl(url: string): void;
downloadFile({ url: string }): void;
openClientSettings(): void;
getPlugins(): (() => Promise<any>)[];
AppHost: {
init(): void;
getDefaultLayout(): 'tv' | 'desktop';
supports(command: string): boolean;
getDeviceProfile(): DeviceProfile;
getSyncProfile(): DeviceProfile;
appName(): string;
appVersion(): string;
deviceName(): string;
exit(): void;
}
}
export const NativeShell = globalThis['NativeShell'] as NativeShell;
export const initCompleted = globalThis['initCompleted'] as Promise<void>;
async function baseApiOperation<T>(section: string, key: string, args: any[] = []): Promise<T> {
await initCompleted;
return await new Promise(resolve => {
globalThis['api'][section][key](...args, resolve);
});
}
interface Signal<T> {
connect: (callback: T) => void;
disconnect: (callback: T) => void;
}
class SignalWrapper<T> implements Signal<T> {
private section: string;
private key: string;
constructor(section: string, key: string) {
this.section = section;
this.key = key;
}
async connect(callback: T) {
await initCompleted;
globalThis['api'][this.section][this.key].connect(callback);
}
async disconnect(callback: T) {
await initCompleted;
globalThis['api'][this.section][this.key].disconnect(callback);
}
}
interface PositionData {
startMilliseconds: number;
autoplay: boolean;
}
interface StreamData {
type: 'video' | 'audio';
headers: Record<string, string>;
metadata: any;
media: any;
}
export const api = {
system: {
/**
* Indicates initialization of webapp is complete and it is ready for inputs
*/
hello(name: string) {
return baseApiOperation<void>('system', 'hello', [name]);
}
},
power: {
setScreensaverEnabled(enabled: boolean) {
return baseApiOperation<void>('power', 'setScreensaverEnabled', [enabled]);
},
},
player: {
/**
* Start playback of media. You need to listen for events to know if this was successful.
* @param {string} url - The url of the media to play
* @param {PositionData} positiondata - Start position and autoplay settings
* @param {StreamData} streamdata - Stream metadata
* @param {string} audioStream - '#{streamindex}' for instance '#1', relative audio only index
* @param {string} subtitleStream - '#{streamindex}' for internal, relative subtitle only index or '#,{url}' for external subtitles '' for no subtitles
*/
load(url: string, positiondata: PositionData, streamdata: StreamData, audioStream: string, subtitleStream: string) {
return baseApiOperation<void>('player', 'load', [url, positiondata, streamdata, audioStream, subtitleStream]);
},
/**
* @param {string} subtitleStream - '#{streamindex}' for internal, relative subtitle only index or '#,{url}' for external subtitles '' for no subtitles
*/
setSubtitleStream(subtitleStream: string) {
return baseApiOperation<void>('player', 'setSubtitleStream', [subtitleStream]);
},
setSubtitleDelay(msDelay: number) {
return baseApiOperation<void>('player', 'setSubtitleDelay', [msDelay]);
},
/**
* @param {string} audioStream - '#{streamindex}' for instance '#1', relative audio only index
*/
setAudioStream(audioStream: string) {
return baseApiOperation<void>('player', 'setAudioStream', [audioStream]);
},
stop() {
return baseApiOperation<void>('player', 'stop');
},
seekTo(positionMs: number) {
return baseApiOperation<void>('player', 'seekTo', [positionMs]);
},
/**
* @returns {Promise<number>} - Position in milliseconds
*/
getPosition() {
return baseApiOperation<number>('player', 'getPosition');
},
pause() {
return baseApiOperation<void>('player', 'pause');
},
play() {
return baseApiOperation<void>('player', 'play');
},
/**
* @param {int} playbackRate - 1000 = normal, 2000 = double speed, 500 = half speed, etc
*/
setPlaybackRate(playbackRate: number) {
return baseApiOperation<void>('player', 'setPlaybackRate', [playbackRate]);
},
/**
* @param {int} volume - 0-100
*/
setVolume(volume: number) {
return baseApiOperation<void>('player', 'setVolume', [volume]);
},
setMuted(muted: boolean) {
return baseApiOperation<void>('player', 'setMuted', [muted]);
},
playing: new SignalWrapper<() => void>('player', 'playing'),
positionUpdate: new SignalWrapper<(position: number) => void>('player', 'positionUpdate'),
finished: new SignalWrapper<() => void>('player', 'finished'),
updateDuration: new SignalWrapper<(duration: number) => void>('player', 'updateDuration'),
error: new SignalWrapper<(error: string) => void>('player', 'error'),
paused: new SignalWrapper<() => void>('player', 'paused'),
},
input: {
/**
* Listen for input events. You must call hello() first before events come in.
* @param {string[]} actions - Array of actions from input devices
*/
hostInput: new SignalWrapper<(actions: string[]) => void>('input', 'hostInput'),
},
settings: {
settingDescriptions() {
return baseApiOperation<{ key: string, settings: JMPSettingInfo[] }>('settings', 'settingDescriptions');
},
allValues(section?: string) {
return baseApiOperation<Record<string, any>>('settings', 'allValues', [section]);
},
value(section: string, key: string) {
return baseApiOperation<void>('settings', 'value', [section, key]);
},
setValue(section: string, key: string, value: any) {
return baseApiOperation<void>('settings', 'setValue', [section, key, value]);
}
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment