Documentation/Source example
Example source
samples/basic/Audio Stream Selector.js
Referenced by:fb.GetAudioStreams
window.DefineScript('Audio Stream Selector');
// Visual selector for audio streams exposed by the input decoder.
// The panel follows the focused playlist item. Click a stream card to select it.
// If the focused item is currently playing, SetAudioStream() restarts playback
// automatically when the effective stream changes.
include("docs/Helpers.js");
const MARGIN = 12;
const HEADER_HEIGHT = 34;
const CARD_GAP = 8;
const CARD_PADDING = 10;
const CARD_HEADER_HEIGHT = 24;
const INFO_LINE_HEIGHT = 17;
const SCROLL_STEP = 48;
const BG = RGB(24, 24, 24);
const CARD = RGB(38, 38, 38);
const CARD_HOVER = RGB(46, 46, 46);
const CARD_SELECTED = RGB(53, 70, 43);
const BORDER = RGB(70, 70, 70);
const BORDER_SELECTED = RGB(126, 170, 85);
const TEXT = RGB(230, 230, 230);
const TEXT_DIM = RGB(165, 165, 165);
const TEXT_ACCENT = RGB(190, 220, 145);
const ERROR = RGB(235, 125, 125);
const fontTitle = gdi.Font('Segoe UI', 15, 1);
const fontHeader = gdi.Font('Segoe UI', 13, 1);
const fontText = gdi.Font('Segoe UI', 12);
const fontSmall = gdi.Font('Segoe UI', 11);
const tfTitle = fb.TitleFormat('%title%[ - %artist%]');
let handle = null;
let streams = [];
let rows = [];
let hoverIndex = -1;
let scrollY = 0;
let contentHeight = 0;
let errorText = '';
function clampScroll() {
const viewport = Math.max(0, window.Height - HEADER_HEIGHT - MARGIN);
const maxScroll = Math.max(0, contentHeight - viewport);
scrollY = Math.max(0, Math.min(scrollY, maxScroll));
}
function streamCardHeight(stream) {
return CARD_PADDING * 2 + CARD_HEADER_HEIGHT + stream.info.InfoCount * INFO_LINE_HEIGHT;
}
function rebuildRows() {
rows = [];
let y = HEADER_HEIGHT + MARGIN;
for (const stream of streams) {
const height = streamCardHeight(stream);
rows.push({ index: stream.index, y, height });
y += height + CARD_GAP;
}
contentHeight = Math.max(0, y - (HEADER_HEIGHT + MARGIN));
clampScroll();
}
function refresh() {
handle = fb.GetFocusItem();
streams = [];
errorText = '';
hoverIndex = -1;
scrollY = 0;
if (handle) {
try {
streams = fb.GetAudioStreams(handle);
}
catch (e) {
errorText = e instanceof Error ? e.message : String(e);
}
}
rebuildRows();
window.Repaint();
}
function refreshStreamsOnly() {
if (!handle) return;
try {
streams = fb.GetAudioStreams(handle);
errorText = '';
}
catch (e) {
streams = [];
errorText = e instanceof Error ? e.message : String(e);
}
rebuildRows();
window.Repaint();
}
function getRowAt(x, y) {
if (x < MARGIN || x >= window.Width - MARGIN) return null;
const contentY = y + scrollY;
for (const row of rows) {
if (contentY >= row.y && contentY < row.y + row.height) {
return row;
}
}
return null;
}
function drawHeader(gr) {
const title = handle ? tfTitle.EvalWithMetadb(handle) : 'No focused item';
gr.GdiDrawText(title, fontTitle, TEXT, MARGIN, 7, Math.max(0, window.Width - MARGIN * 2), 24, 0x00000020 | 0x00008000);
}
function drawMessage(gr, text, colour) {
gr.GdiDrawText(text, fontText, colour, MARGIN, HEADER_HEIGHT + MARGIN,
Math.max(0, window.Width - MARGIN * 2), Math.max(0, window.Height - HEADER_HEIGHT - MARGIN * 2),
0x00000010);
}
function drawStream(gr, stream, row) {
const x = MARGIN;
const y = row.y - scrollY;
const w = Math.max(0, window.Width - MARGIN * 2);
const h = row.height;
if (y + h < HEADER_HEIGHT || y > window.Height) return;
const hovered = hoverIndex === stream.index;
const fill = stream.selected ? CARD_SELECTED : (hovered ? CARD_HOVER : CARD);
const border = stream.selected ? BORDER_SELECTED : BORDER;
gr.FillSolidRect(x, y, w, h, fill);
gr.DrawRect(x, y, w - 1, h - 1, 1, border);
let flags = [];
if (stream.selected) flags.push('selected');
if (stream.default) flags.push('default');
const heading = `Stream ${stream.index}${flags.length ? ` [${flags.join(', ')}]` : ''}`;
gr.GdiDrawText(heading, fontHeader, stream.selected ? TEXT_ACCENT : TEXT,
x + CARD_PADDING, y + CARD_PADDING, Math.max(0, w - CARD_PADDING * 2), CARD_HEADER_HEIGHT,
0x00000020 | 0x00008000);
let infoY = y + CARD_PADDING + CARD_HEADER_HEIGHT;
for (let i = 0; i < stream.info.InfoCount; ++i) {
const name = stream.info.InfoName(i);
const value = stream.info.InfoValue(i);
gr.GdiDrawText(`${name}: ${value}`, fontSmall, TEXT_DIM,
x + CARD_PADDING, infoY, Math.max(0, w - CARD_PADDING * 2), INFO_LINE_HEIGHT,
0x00000020 | 0x00008000);
infoY += INFO_LINE_HEIGHT;
}
}
function on_paint(gr) {
gr.FillSolidRect(0, 0, window.Width, window.Height, BG);
drawHeader(gr);
if (!handle) {
drawMessage(gr, 'Focus a playlist item to inspect its audio streams.', TEXT_DIM);
return;
}
if (errorText) {
drawMessage(gr, `GetAudioStreams failed:\n${errorText}`, ERROR);
return;
}
if (!streams.length) {
drawMessage(gr, 'The input decoder does not expose selectable audio streams for this file.', TEXT_DIM);
return;
}
for (let i = 0; i < streams.length; ++i) {
drawStream(gr, streams[i], rows[i]);
}
}
function on_mouse_move(x, y) {
const row = getRowAt(x, y);
const next = row ? row.index : -1;
if (next !== hoverIndex) {
hoverIndex = next;
window.Repaint();
}
}
function on_mouse_leave() {
if (hoverIndex !== -1) {
hoverIndex = -1;
window.Repaint();
}
}
function on_mouse_lbtn_up(x, y) {
const row = getRowAt(x, y);
if (!row || !handle) return;
const stream = streams.find(item => item.index === row.index);
if (!stream || stream.selected) return;
try {
fb.SetAudioStream(handle, stream.index);
refreshStreamsOnly();
}
catch (e) {
errorText = e instanceof Error ? e.message : String(e);
window.Repaint();
}
}
function on_mouse_wheel(step) {
scrollY -= step * SCROLL_STEP;
clampScroll();
window.Repaint();
}
function on_size() {
clampScroll();
}
function on_item_focus_change() {
refresh();
}
function on_playlist_switch() {
refresh();
}
refresh();