ImageStream

Introduction

vtkImageStream.

Methods

connect

createViewStream

Argument Type Required Description
viewId String No The ID of the view.
size Size No The size of the view.

delete

disconnect

extend

Method used to decorate a given object (publicAPI+model) with vtkImageStream characteristics.

Argument Type Required Description
publicAPI Yes object on which methods will be bounds (public)
model Yes object on which data structure will be bounds (protected)
initialValues IImageStreamInitialValues No (default: {})

getProtocol

getServerAnimationFPS

newInstance

Method used to create a new instance of vtkImageStream

Argument Type Required Description
initialValues IImageStreamInitialValues No for pre-setting some of its content

registerViewStream

setServerAnimationFPS

Argument Type Required Description
serverAnimationFPS Yes

unregisterViewStream

Source

DefaultProtocol.d.ts
import { Size, Vector3 } from '../../../types';

declare function createMethods(session: any): {
subscribeToImageStream: (callback: any) => any;
unsubscribeToImageStream: (subscription: any) => any;
registerView: (viewId: string) => any;
unregisterView: (viewId: string) => any;
enableView: (viewId: string, enabled: boolean) => any;
render: (options?: { size: Size; view: number }) => any;
resetCamera: (view?: number) => any;
invalidateCache: (viewId: string) => any;
setQuality: (viewId: string, quality: number, ratio?: number) => any;
setSize: (viewId: string, width?: number, height?: number) => any;
setServerAnimationFPS: (fps?: number) => any;
getServerAnimationFPS: () => number;
startAnimation: (viewId?: number) => any;
stopAnimation: (viewId?: number) => any;
updateCamera: (
viewId: string,
focalPoint: Vector3,
viewUp: Vector3,
position: Vector3,
forceUpdate?: boolean
) => any;
updateCameraParameters: (
viewId?: number,
parameters?: {},
forceUpdate?: boolean
) => any;
};

export default createMethods;
DefaultProtocol.js
export default function createMethods(session) {
return {
subscribeToImageStream: (callback) =>
session.subscribe('viewport.image.push.subscription', callback),
unsubscribeToImageStream: (subscription) =>
session.unsubscribe(subscription),
registerView: (viewId) =>
session.call('viewport.image.push.observer.add', [viewId]),
unregisterView: (viewId) =>
session.call('viewport.image.push.observer.remove', [viewId]),
enableView: (viewId, enabled) =>
session.call('viewport.image.push.enabled', [viewId, enabled]),
render: (options = { size: [400, 400], view: -1 }) =>
session.call('viewport.image.push', [options]),
resetCamera: (view = -1) => session.call('viewport.camera.reset', [view]),
invalidateCache: (viewId) =>
session.call('viewport.image.push.invalidate.cache', [viewId]),
setQuality: (viewId, quality, ratio = 1) =>
session.call('viewport.image.push.quality', [viewId, quality, ratio]),
setSize: (viewId, width = 400, height = 400) =>
session.call('viewport.image.push.original.size', [
viewId,
width,
height,
]),
setServerAnimationFPS: (fps = 30) =>
session.call('viewport.image.animation.fps.max', [fps]),
getServerAnimationFPS: () =>
session.call('viewport.image.animation.fps.get', []),
startAnimation: (viewId = -1) =>
session.call('viewport.image.animation.start', [viewId]),
stopAnimation: (viewId = -1) =>
session.call('viewport.image.animation.stop', [viewId]),
updateCamera: (viewId, focalPoint, viewUp, position, forceUpdate = true) =>
session.call('viewport.camera.update', [
viewId ?? -1,
focalPoint,
viewUp,
position,
forceUpdate,
]),
updateCameraParameters: (
viewId = -1,
parameters = {},
forceUpdate = true
) =>
session.call('viewport.camera.update.params', [
viewId,
parameters,
forceUpdate,
]),
};
}
ViewStream.d.ts
import { vtkObject } from '../../../interfaces';
import { Size } from '../../../types';
import vtkCamera from '../../../Rendering/Core/Camera';
import DefaultProtocol from './DefaultProtocol';
/**
*
*/
export interface IViewStreamInitialValues {
protocol?: typeof DefaultProtocol;
api?: any;
cameraUpdateRate?: number;
decodeImage?: boolean;
fpsWindowSize?: number;
interactiveQuality?: number;
interactiveRatio?: number;
isAnimating?: boolean;
mimeType?: string;
size?: Size;
stillQuality?: number;
stillRatio?: number;
useCameraParameters?: boolean;
viewId?: string;
}

interface IMetaData {
size: Size;
id: string;
memory: number;
workTime: number;
}

interface IEvent {
url: string;
fps: number[];
metadata: IMetaData;
}

export interface vtkViewStream extends vtkObject {
/**
*
* @param callback
*/
onImageReady(callback: () => void): any;

/**
*
*/
getViewId(): string;

/**
*
*/
getSize(): Size;

/**
*
*/
getFps(): number[];

/**
*
*/
getLastImageEvent(): IEvent;

/**
*
*/
getCamera(): vtkCamera;

/**
*
* @param camera
*/
setCamera(camera: vtkCamera): boolean;

/**
*
*/
getCameraUpdateRate(): number;

/**
*
* @param cameraUpdateRate
*/
setCameraUpdateRate(cameraUpdateRate: number): boolean;

/**
*
*/
getDecodeImage(): boolean;

/**
*
* @param decodeImage
*/
setDecodeImage(decodeImage: boolean): boolean;

/**
*
*/
getFpsWindowSize(): number;

/**
*
* @param fpsWindowSize
*/
setFpsWindowSize(fpsWindowSize: number): boolean;

/**
*
*/
getInteractiveQuality(): number;

/**
*
* @param interactiveQuality
*/
setInteractiveQuality(interactiveQuality: number): boolean;

/**
*
*/
getInteractiveRatio(): number;

/**
*
* @param interactiveRatio
*/
setInteractiveRatio(interactiveRatio: number): boolean;

/**
*
*/
getStillQuality(): number;

/**
*
* @param stillQuality
*/
setStillQuality(stillQuality: number): boolean;

/**
*
*/
getStillRatio(): number;

/**
*
* @param stillRatio
*/
setStillRatio(stillRatio: number): boolean;

/**
*
*/
getUseCameraParameters(): boolean;

/**
*
* @param useCameraParameters
*/
setUseCameraParameters(useCameraParameters: boolean): boolean;

/**
*
*/
pushCamera(): any;

/**
*
*/
invalidateCache(): any;

/**
*
*/
render(): any;

/**
*
*/
resetCamera(): any;

/**
*
*/
startAnimation(): any;

/**
*
*/
stopAnimation(): any;

/**
*
* @param width
* @param height
*/
setSize(width: number, height: number): any;

/**
*
*/
startInteraction(): any;

/**
*
*/
endInteraction(): any;

/**
*
* @param viewId
*/
setViewId(viewId: string): boolean;

/**
*
* @param msg
*/
processMessage(msg: any): void;

/**
*
*/
delete(): void;
}

/**
* Method used to decorate a given object (publicAPI+model) with vtkViewStream characteristics.
* @param publicAPI
* @param model
* @param initialValues
*/
export function extend(
publicAPI: object,
model: object,
initialValues?: IViewStreamInitialValues
): void;

/**
* Method used to create a new instance of vtkViewStream
* @param {IViewStreamInitialValues} [initialValues] for pre-setting some of its content
*/
export function newInstance(
initialValues?: IViewStreamInitialValues
): vtkViewStream;

/**
* IViewStreamInitialValues provides a way to create a remote view.
*/
export declare const vtkViewStream: {
newInstance: typeof newInstance;
extend: typeof extend;
};
export default vtkViewStream;
ViewStream.js
import macro from 'vtk.js/Sources/macros';

function vtkViewStream(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkViewStream');

// Internal variables
model.imageDecodingPool = [new Image(), new Image()];
model.eventPool = [];
model.nextPoolImageIndex = 0;
model.urlToRevoke = [];
model.activeURL = null;
model.fps = [];
model.lastTime = Date.now();
model.lastImageEvent = null;

// --------------------------------------------------------------------------
// Internal methods
// --------------------------------------------------------------------------

function imageLoaded(e) {
const id = Number(this.dataset.id);
publicAPI.invokeImageReady(model.eventPool[id]);
}

// --------------------------------------------------------------------------

function prepareDecodingPool(size = 2) {
while (model.imageDecodingPool.length < size) {
model.imageDecodingPool.push(new Image());
}
for (let i = 0; i < model.imageDecodingPool.length; i++) {
model.imageDecodingPool[i].dataset.id = i;
model.imageDecodingPool[i].onload = imageLoaded;
}
}

// --------------------------------------------------------------------------

function decodeImage(event) {
model.eventPool[model.nextPoolImageIndex] = event;
event.image = model.imageDecodingPool[model.nextPoolImageIndex++];
model.nextPoolImageIndex %= model.imageDecodingPool.length;
event.image.src = event.url;
}

// --------------------------------------------------------------------------

publicAPI.pushCamera = () => {
const focalPoint = model.camera.getReferenceByName('focalPoint');
const viewUp = model.camera.getReferenceByName('viewUp');
const position = model.camera.getReferenceByName('position');
const parallelProjection = model.camera.getParallelProjection();
const viewAngle = model.camera.getViewAngle();
const parallelScale = model.camera.getParallelScale();
let promise = null;

if (model.useCameraParameters) {
promise = model.protocol.updateCameraParameters(
model.viewId,
{
focalPoint,
viewUp,
position,
parallelProjection,
viewAngle,
parallelScale,
},
false
);
} else {
promise = model.protocol.updateCamera(
model.viewId,
focalPoint,
viewUp,
position,
false
);
}

if (model.isAnimating) {
setTimeout(publicAPI.pushCamera, 1000 / model.cameraUpdateRate);
}
return promise;
};

// --------------------------------------------------------------------------

publicAPI.invalidateCache = () =>
model.protocol.invalidateCache(model.viewId);

// --------------------------------------------------------------------------
// PublicAPI
// --------------------------------------------------------------------------

publicAPI.render = () =>
model.protocol.render({ view: model.viewId, size: model.size });

// --------------------------------------------------------------------------

publicAPI.resetCamera = () => model.protocol.resetCamera(model.viewId);

// --------------------------------------------------------------------------

publicAPI.startAnimation = () => model.protocol.startAnimation(model.viewId);

// --------------------------------------------------------------------------

publicAPI.stopAnimation = () => model.protocol.stopAnimation(model.viewId);

// --------------------------------------------------------------------------

publicAPI.setSize = (width, height) => {
let changeDetected = false;
if (model.size[0] !== width || model.size[1] !== height) {
model.size = [width, height];
changeDetected = true;
}

if (changeDetected) {
publicAPI.modified();
if (model.protocol) {
return model.protocol.setSize(model.viewId, width, height);
}
}

return Promise.resolve(false);
};

// --------------------------------------------------------------------------

publicAPI.startInteraction = () => {
const promises = [
model.protocol.setQuality(
model.viewId,
model.interactiveQuality,
model.interactiveRatio
),
];

if (model.camera) {
promises.push(publicAPI.startAnimation());
model.isAnimating = true;
promises.push(publicAPI.pushCamera());
}

return Promise.all(promises);
};

// --------------------------------------------------------------------------

publicAPI.endInteraction = () => {
const promises = [];
promises.push(
model.protocol.setQuality(
model.viewId,
model.stillQuality,
model.stillRatio
)
);
if (model.camera) {
promises.push(publicAPI.stopAnimation());
model.isAnimating = false;
promises.push(publicAPI.pushCamera());
} else {
promises.push(publicAPI.render());
}

return Promise.all(promises);
};

// --------------------------------------------------------------------------

publicAPI.setViewId = (id) => {
if (model.viewId === id || !model.protocol) {
return false;
}
if (model.viewId) {
model.protocol.unregisterView(model.viewId);
}
model.viewId = id;
if (model.viewId) {
model.protocol.registerView(model.viewId).then(({ viewId }) => {
model.viewId = viewId;
});
}
return true;
};

// --------------------------------------------------------------------------

publicAPI.processMessage = (msg) => {
/* eslint-disable eqeqeq */
if (msg.id != model.viewId) {
return;
}
/* eslint-enable eqeqeq */
const imgBlob = new Blob([msg.image], {
type: model.mimeType,
});
if (model.activeURL) {
model.urlToRevoke.push(model.activeURL);
model.activeURL = null;
while (model.urlToRevoke.length > 60) {
const url = model.urlToRevoke.shift();
window.URL.revokeObjectURL(url);
}
}
model.activeURL = URL.createObjectURL(imgBlob);
const time = Date.now();
const fps = Math.floor(10000 / (time - model.lastTime)) / 10;
model.fps.push(fps);
model.lastTime = time;

model.lastImageEvent = {
url: model.activeURL,
fps,
metadata: {
size: msg.size,
id: msg.id,
memory: msg.memsize,
workTime: msg.workTime,
},
};
if (model.decodeImage) {
decodeImage(model.lastImageEvent);
} else {
publicAPI.invokeImageReady(model.lastImageEvent);
}

// GC fps
while (model.fps.length > model.fpsWindowSize) {
model.fps.shift();
}
};

// --------------------------------------------------------------------------

publicAPI.delete = macro.chain(() => {
model.unregisterViewStream(publicAPI);
publicAPI.setViewId(null);
while (model.urlToRevoke.length) {
window.URL.revokeObjectURL(model.urlToRevoke.pop());
}
}, publicAPI.delete);

// --------------------------------------------------------------------------
// Initialize object
// --------------------------------------------------------------------------

prepareDecodingPool();
}

// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------

const DEFAULT_VALUES = {
// protocol: null,
// api: null,
cameraUpdateRate: 30,
decodeImage: true,
fpsWindowSize: 250,
interactiveQuality: 80,
interactiveRatio: 1,
isAnimating: false,
mimeType: 'image/jpeg',
size: [-1, -1],
stillQuality: 100,
stillRatio: 1,
useCameraParameters: false,
viewId: null,
};

// ----------------------------------------------------------------------------

export function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, DEFAULT_VALUES, initialValues);

// Object methods
macro.obj(publicAPI, model);
macro.event(publicAPI, model, 'ImageReady');
macro.get(publicAPI, model, ['viewId', 'size', 'fps', 'lastImageEvent']);
macro.setGet(publicAPI, model, [
'camera',
'cameraUpdateRate',
'decodeImage',
'fpsWindowSize',
'interactiveQuality',
'interactiveRatio',
'stillQuality',
'stillRatio',
'useCameraParameters',
]);

// Object specific methods
vtkViewStream(publicAPI, model);

// Blend APIs
Object.assign(publicAPI, model.sharedAPI);
}

// ----------------------------------------------------------------------------

export const newInstance = macro.newInstance(extend, 'vtkViewStream');

// ----------------------------------------------------------------------------

export default { newInstance, extend };
index.d.ts
import { vtkObject } from '../../../interfaces';
import { Size } from '../../../types';
import vtkViewStream from './ViewStream';

/**
*
*/
export interface IImageStreamInitialValues {
viewStreams?: any[];
serverAnimationFPS?: number;
}

// Return type of wslink/src/WebsocketConnection, getSession() method.
type WebsocketSession = any;

export interface vtkImageStream extends vtkObject {
/**
*
*/
connect(session: WebsocketSession): void;

/**
*
* @param {String} [viewId] The ID of the view.
* @param {Size} [size] The size of the view.
*/
createViewStream(viewId?: string, size?: Size): vtkViewStream;

/**
*
*/
delete(): void;

/**
*
*/
disconnect(): void;

/**
*
*/
getProtocol(): any;

/**
*
*/
getServerAnimationFPS(): number;

/**
*
*/
registerViewStream(): void;

/**
*
* @param serverAnimationFPS
*/
setServerAnimationFPS(serverAnimationFPS: number): boolean;

/**
*
*/
unregisterViewStream(): void;
}

/**
* Method used to decorate a given object (publicAPI+model) with vtkImageStream characteristics.
*
* @param publicAPI object on which methods will be bounds (public)
* @param model object on which data structure will be bounds (protected)
* @param {IImageStreamInitialValues} [initialValues] (default: {})
*/
export function extend(
publicAPI: object,
model: object,
initialValues?: IImageStreamInitialValues
): void;

/**
* Method used to create a new instance of vtkImageStream
* @param {IImageStreamInitialValues} [initialValues] for pre-setting some of its content
*/
export function newInstance(
initialValues?: IImageStreamInitialValues
): vtkImageStream;

/**
* vtkImageStream.
*/
export declare const vtkImageStream: {
newInstance: typeof newInstance;
extend: typeof extend;
};
export default vtkImageStream;
index.js
import macro from 'vtk.js/Sources/macros';
import DefaultProtocol from 'vtk.js/Sources/IO/Core/ImageStream/DefaultProtocol';
import ViewStream from 'vtk.js/Sources/IO/Core/ImageStream/ViewStream';

function vtkImageStream(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkImageStream');

// --------------------------------------------------------------------------
// Internal private method
// --------------------------------------------------------------------------

function onImage(data) {
const message = data[0];
if (!message || !message.image) {
return;
}
for (let i = 0; i < model.viewStreams.length; i++) {
model.viewStreams[i].processMessage(message);
}
}

// --------------------------------------------------------------------------
// PublicAPI
// --------------------------------------------------------------------------

publicAPI.setServerAnimationFPS = (maxFPS = 30) => {
let changeDetected = false;
if (model.serverAnimationFPS !== maxFPS) {
model.serverAnimationFPS = maxFPS;
changeDetected = true;
}

if (!model.protocol) {
return Promise.resolve(true);
}

if (changeDetected) {
publicAPI.modified();
}

return model.protocol.setServerAnimationFPS(maxFPS);
};

// --------------------------------------------------------------------------

publicAPI.connect = (session, protocol = DefaultProtocol) => {
if (model.connected || !session || !protocol) {
return;
}
model.protocol = protocol(session);
model.protocol
.subscribeToImageStream(onImage)
.promise // new API in wslink 1.0.5+
.then((subscription) => {
model.renderTopicSubscription = subscription;
model.connected = true;
})
.catch((e) => {
model.connected = false;
console.error(e);
});
};

// --------------------------------------------------------------------------

publicAPI.disconnect = () => {
if (model.protocol && model.connected && model.renderTopicSubscription) {
model.protocol.unsubscribeToImageStream(model.renderTopicSubscription);
model.renderTopicSubscription = null;
}
model.connected = false;
};

// --------------------------------------------------------------------------

publicAPI.registerViewStream = (view) => {
model.viewStreams.push(view);
};

// --------------------------------------------------------------------------

publicAPI.unregisterViewStream = (view) => {
model.viewStreams = model.viewStreams.filter((v) => v !== view);
};

// --------------------------------------------------------------------------

publicAPI.createViewStream = (viewId = '-1', size = [400, 400]) => {
const {
setServerAnimationFPS,
getServerAnimationFPS,
unregisterViewStream,
} = publicAPI;
const viewStream = ViewStream.newInstance({
protocol: model.protocol,
unregisterViewStream,
sharedAPI: {
setServerAnimationFPS,
getServerAnimationFPS,
},
});
viewStream.setViewId(viewId);
viewStream.setSize(size[0], size[1]);
publicAPI.registerViewStream(viewStream);

return viewStream;
};

// --------------------------------------------------------------------------

publicAPI.delete = macro.chain(() => {
while (model.viewStreams.length) {
model.viewStreams.pop().delete();
}
publicAPI.disconnect();
}, publicAPI.delete);
}

// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------

const DEFAULT_VALUES = {
// protocol: null,
viewStreams: [],
serverAnimationFPS: -1,
};

// ----------------------------------------------------------------------------

export function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, DEFAULT_VALUES, initialValues);

// Object methods
macro.obj(publicAPI, model);
macro.get(publicAPI, model, ['serverAnimationFPS', 'protocol']);

// Object specific methods
vtkImageStream(publicAPI, model);
}

// ----------------------------------------------------------------------------

export const newInstance = macro.newInstance(extend, 'vtkImageStream');

// ----------------------------------------------------------------------------

export default { newInstance, extend };