Last active
March 15, 2023 05:07
-
-
Save iwalton3/8f0f4c5caa51b08975e9b61a0a0047bc to your computer and use it in GitHub Desktop.
Prototype Jellyfin Media Player Typescript API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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