fix(dicom-pdf): enhance PDF loading with authentication support
- move authenticated rendered media loading into the datasource\n- route DICOM video display sets through Cornerstone video viewports\n- add a 3.12 to 3.13 migration note for the removed DICOM-video viewport namespace
This commit is contained in:
parent
2800f82e3c
commit
e0f42d4f1d
@ -18,6 +18,7 @@ import getDirectURL from '../utils/getDirectURL';
|
||||
import { fixBulkDataURI } from './utils/fixBulkDataURI';
|
||||
import { HeadersInterface } from '@ohif/core/src/types/RequestHeaders';
|
||||
import { getGetThumbnailSrc, ThumbnailContext } from './retrieveThumbnail';
|
||||
import { getRenderedURL } from './retrieveRendered';
|
||||
|
||||
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
|
||||
|
||||
@ -108,7 +109,6 @@ export type BulkDataURIConfig = {
|
||||
relativeResolution?: 'studies' | 'series';
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The header options are the options passed into the generateWadoHeader
|
||||
* command. This takes an extensible set of attributes to allow future enhancements.
|
||||
@ -291,6 +291,14 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) {
|
||||
params
|
||||
);
|
||||
},
|
||||
renderedURL: (params, options) => {
|
||||
return getRenderedURL({
|
||||
config: dicomWebConfig,
|
||||
getAuthorizationHeader,
|
||||
retrieve: implementation.retrieve,
|
||||
userAuthenticationService,
|
||||
})(params, options);
|
||||
},
|
||||
/**
|
||||
* Provide direct access to the dicom web client for certain use cases
|
||||
* where the dicom web client is used by an external library such as the
|
||||
|
||||
@ -0,0 +1,147 @@
|
||||
import { fetchRenderedURL, getRenderedURL, isTrustedWadoURL } from './retrieveRendered';
|
||||
|
||||
describe('retrieveRendered', () => {
|
||||
const originalFetch = global.fetch;
|
||||
const originalCreateObjectURL = URL.createObjectURL;
|
||||
const originalRevokeObjectURL = URL.revokeObjectURL;
|
||||
|
||||
beforeEach(() => {
|
||||
global.fetch = jest.fn();
|
||||
URL.createObjectURL = jest.fn(() => 'blob:rendered') as jest.Mock;
|
||||
URL.revokeObjectURL = jest.fn() as jest.Mock;
|
||||
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
global.fetch = originalFetch;
|
||||
URL.createObjectURL = originalCreateObjectURL;
|
||||
URL.revokeObjectURL = originalRevokeObjectURL;
|
||||
});
|
||||
|
||||
it('returns null when there is no resolved URL', async () => {
|
||||
const result = await fetchRenderedURL({
|
||||
url: undefined,
|
||||
wadoRoot: 'https://example.com/dicomweb',
|
||||
headers: { Authorization: 'Bearer token' },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ url: null });
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns the direct URL without fetching when there is no authorization header', async () => {
|
||||
const result = await fetchRenderedURL({
|
||||
url: 'https://example.com/dicomweb/rendered',
|
||||
wadoRoot: 'https://example.com/dicomweb',
|
||||
headers: {},
|
||||
});
|
||||
|
||||
expect(result).toEqual({ url: 'https://example.com/dicomweb/rendered' });
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not attach auth headers to untrusted absolute URLs', async () => {
|
||||
const result = await fetchRenderedURL({
|
||||
url: 'https://untrusted.example.com/rendered',
|
||||
wadoRoot: 'https://example.com/dicomweb',
|
||||
headers: { Authorization: 'Bearer token' },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ url: 'https://untrusted.example.com/rendered' });
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fetches trusted URLs with auth and returns a revocable object URL', async () => {
|
||||
(global.fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
blob: jest.fn().mockResolvedValueOnce(new Blob(['pdf'], { type: 'application/pdf' })),
|
||||
});
|
||||
|
||||
const result = await fetchRenderedURL({
|
||||
url: 'https://example.com/dicomweb/studies/1/rendered',
|
||||
wadoRoot: 'https://example.com/dicomweb',
|
||||
headers: { Authorization: 'Bearer token' },
|
||||
});
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://example.com/dicomweb/studies/1/rendered',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
headers: { Authorization: 'Bearer token' },
|
||||
})
|
||||
);
|
||||
expect(result.url).toBe('blob:rendered');
|
||||
|
||||
result.revoke?.();
|
||||
result.revoke?.();
|
||||
|
||||
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(1);
|
||||
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:rendered');
|
||||
});
|
||||
|
||||
it('returns null and triggers unauthenticated handling on 401/403 responses', async () => {
|
||||
const handleUnauthenticated = jest.fn();
|
||||
(global.fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 401,
|
||||
});
|
||||
|
||||
const result = await fetchRenderedURL({
|
||||
url: 'https://example.com/dicomweb/studies/1/rendered',
|
||||
wadoRoot: 'https://example.com/dicomweb',
|
||||
headers: { Authorization: 'Bearer token' },
|
||||
userAuthenticationService: { handleUnauthenticated },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ url: null });
|
||||
expect(handleUnauthenticated).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('revokes an object URL created after the request was aborted', async () => {
|
||||
const abortController = new AbortController();
|
||||
(global.fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
blob: jest.fn().mockImplementationOnce(async () => {
|
||||
abortController.abort();
|
||||
return new Blob(['pdf'], { type: 'application/pdf' });
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await fetchRenderedURL({
|
||||
url: 'https://example.com/dicomweb/studies/1/rendered',
|
||||
wadoRoot: 'https://example.com/dicomweb',
|
||||
headers: { Authorization: 'Bearer token' },
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ url: null });
|
||||
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:rendered');
|
||||
});
|
||||
|
||||
it('resolves directURL through the datasource retrieve API', async () => {
|
||||
const renderedURL = getRenderedURL({
|
||||
config: { wadoRoot: 'https://example.com/dicomweb' },
|
||||
getAuthorizationHeader: () => ({}),
|
||||
retrieve: {
|
||||
directURL: jest.fn().mockResolvedValueOnce('https://example.com/dicomweb/rendered'),
|
||||
},
|
||||
});
|
||||
|
||||
await expect(renderedURL({ instance: {} })).resolves.toEqual({
|
||||
url: 'https://example.com/dicomweb/rendered',
|
||||
});
|
||||
});
|
||||
|
||||
it('trusts URLs that share the configured WADO origin', () => {
|
||||
expect(
|
||||
isTrustedWadoURL('/dicomweb/studies/1/rendered', `${window.location.origin}/dicomweb`)
|
||||
).toBe(true);
|
||||
expect(
|
||||
isTrustedWadoURL(
|
||||
'https://other.example.com/dicomweb/studies/1/rendered',
|
||||
'https://example.com/dicomweb'
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
141
extensions/default/src/DicomWebDataSource/retrieveRendered.ts
Normal file
141
extensions/default/src/DicomWebDataSource/retrieveRendered.ts
Normal file
@ -0,0 +1,141 @@
|
||||
import { HeadersInterface } from '@ohif/core/src/types/RequestHeaders';
|
||||
|
||||
type RetrieveApi = {
|
||||
directURL: (params: unknown) => unknown;
|
||||
};
|
||||
|
||||
type RenderedURLConfig = {
|
||||
wadoRoot?: string;
|
||||
};
|
||||
|
||||
type UserAuthenticationService = {
|
||||
handleUnauthenticated?: () => unknown;
|
||||
};
|
||||
|
||||
type GetRenderedURLDeps = {
|
||||
config: RenderedURLConfig;
|
||||
getAuthorizationHeader: () => HeadersInterface;
|
||||
retrieve: RetrieveApi;
|
||||
userAuthenticationService?: UserAuthenticationService;
|
||||
};
|
||||
|
||||
type RenderedURLOptions = {
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
type FetchRenderedURLOptions = RenderedURLOptions & {
|
||||
url?: string | null;
|
||||
wadoRoot?: string;
|
||||
headers?: HeadersInterface;
|
||||
userAuthenticationService?: UserAuthenticationService;
|
||||
};
|
||||
|
||||
export type RenderedURLResult = {
|
||||
url: string | null;
|
||||
revoke?: () => void;
|
||||
};
|
||||
|
||||
export function getRenderedURL({
|
||||
config,
|
||||
getAuthorizationHeader,
|
||||
retrieve,
|
||||
userAuthenticationService,
|
||||
}: GetRenderedURLDeps) {
|
||||
return async function renderedURL(
|
||||
params: unknown,
|
||||
options: RenderedURLOptions = {}
|
||||
): Promise<RenderedURLResult> {
|
||||
const resolvedUrl = (await retrieve.directURL(params)) as string | undefined | null;
|
||||
|
||||
return fetchRenderedURL({
|
||||
url: resolvedUrl,
|
||||
wadoRoot: config.wadoRoot,
|
||||
headers: getAuthorizationHeader(),
|
||||
signal: options.signal,
|
||||
userAuthenticationService,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchRenderedURL({
|
||||
url,
|
||||
wadoRoot,
|
||||
headers,
|
||||
signal,
|
||||
userAuthenticationService,
|
||||
}: FetchRenderedURLOptions): Promise<RenderedURLResult> {
|
||||
if (!url || signal?.aborted) {
|
||||
return { url: null };
|
||||
}
|
||||
|
||||
if (!headers?.Authorization || !isTrustedWadoURL(url, wadoRoot)) {
|
||||
return { url };
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: headers as Record<string, string>,
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
userAuthenticationService?.handleUnauthenticated?.();
|
||||
}
|
||||
console.warn(`rendered media fetch failed with status ${response.status}`);
|
||||
return { url: null };
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
|
||||
if (signal?.aborted) {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
return { url: null };
|
||||
}
|
||||
|
||||
return {
|
||||
url: objectUrl,
|
||||
revoke: createRevokeOnce(objectUrl),
|
||||
};
|
||||
} catch (error) {
|
||||
if ((error as { name?: string })?.name === 'AbortError') {
|
||||
return { url: null };
|
||||
}
|
||||
|
||||
console.warn('rendered media fetch failed', error);
|
||||
return { url: null };
|
||||
}
|
||||
}
|
||||
|
||||
export function isTrustedWadoURL(url: string, wadoRoot?: string): boolean {
|
||||
if (!wadoRoot) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedUrl = new URL(url, window.location.href);
|
||||
const parsedWadoRoot = new URL(wadoRoot, window.location.href);
|
||||
const isHttpUrl = parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:';
|
||||
const isHttpWadoRoot =
|
||||
parsedWadoRoot.protocol === 'http:' || parsedWadoRoot.protocol === 'https:';
|
||||
|
||||
return isHttpUrl && isHttpWadoRoot && parsedUrl.origin === parsedWadoRoot.origin;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function createRevokeOnce(objectUrl: string) {
|
||||
let revoked = false;
|
||||
|
||||
return () => {
|
||||
if (revoked) {
|
||||
return;
|
||||
}
|
||||
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
revoked = true;
|
||||
};
|
||||
}
|
||||
@ -52,6 +52,7 @@ function createDicomWebProxyApi(dicomWebProxyConfig, servicesManager: AppTypes.S
|
||||
retrieve: {
|
||||
getGetThumbnailSrc: (...args) => dicomWebDelegate.retrieve.getGetThumbnailSrc(...args),
|
||||
directURL: (...args) => dicomWebDelegate.retrieve.directURL(...args),
|
||||
renderedURL: (...args) => dicomWebDelegate.retrieve.renderedURL(...args),
|
||||
series: {
|
||||
metadata: async (...args) => dicomWebDelegate.retrieve.series.metadata(...args),
|
||||
},
|
||||
@ -62,7 +63,8 @@ function createDicomWebProxyApi(dicomWebProxyConfig, servicesManager: AppTypes.S
|
||||
reject: {
|
||||
series: (...args) => dicomWebDelegate?.reject?.series?.(...args),
|
||||
},
|
||||
deleteStudyMetadataPromise: (...args) => dicomWebDelegate?.deleteStudyMetadataPromise?.(...args),
|
||||
deleteStudyMetadataPromise: (...args) =>
|
||||
dicomWebDelegate?.deleteStudyMetadataPromise?.(...args),
|
||||
getImageIdsForDisplaySet: (...args) => dicomWebDelegate?.getImageIdsForDisplaySet?.(...args),
|
||||
getImageIdsForInstance: (...args) => dicomWebDelegate?.getImageIdsForInstance?.(...args),
|
||||
getConfig: (...args) =>
|
||||
|
||||
@ -242,6 +242,24 @@ function createMergeDataSourceApi(
|
||||
defaultDataSourceName,
|
||||
extensionManager,
|
||||
}),
|
||||
renderedURL: (...args: unknown[]) => {
|
||||
const [dataSource] = extensionManager.getDataSources(defaultDataSourceName);
|
||||
const renderedURL = get(dataSource, 'retrieve.renderedURL');
|
||||
|
||||
if (renderedURL) {
|
||||
return renderedURL.apply(dataSource, args);
|
||||
}
|
||||
|
||||
const directURL = get(dataSource, 'retrieve.directURL');
|
||||
|
||||
if (!directURL) {
|
||||
return Promise.resolve({ url: null });
|
||||
}
|
||||
|
||||
return Promise.resolve(directURL.apply(dataSource, [args[0]])).then(url => ({
|
||||
url: (url as string | undefined | null) || null,
|
||||
}));
|
||||
},
|
||||
series: {
|
||||
metadata: (...args: unknown[]) =>
|
||||
callForAllDataSourcesAsync({
|
||||
|
||||
@ -14,12 +14,17 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager)
|
||||
const { Modality, SOPInstanceUID } = instance;
|
||||
const { SeriesDescription = 'PDF', MIMETypeOfEncapsulatedDocument } = instance;
|
||||
const { SeriesNumber, SeriesDate, SeriesInstanceUID, StudyInstanceUID, SOPClassUID } = instance;
|
||||
const renderedUrl = dataSource.retrieve.directURL({
|
||||
const renderedUrlParams = {
|
||||
instance,
|
||||
tag: 'EncapsulatedDocument',
|
||||
defaultType: MIMETypeOfEncapsulatedDocument || 'application/pdf',
|
||||
singlepart: 'pdf',
|
||||
});
|
||||
};
|
||||
const renderedUrl = dataSource.retrieve.directURL(renderedUrlParams);
|
||||
const getRenderedUrl = dataSource.retrieve.renderedURL
|
||||
? options =>
|
||||
dataSource.retrieve.renderedURL({ ...renderedUrlParams, url: renderedUrl }, options)
|
||||
: undefined;
|
||||
|
||||
const displaySet = {
|
||||
//plugin: id,
|
||||
@ -36,6 +41,7 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager)
|
||||
referencedImages: null,
|
||||
measurements: null,
|
||||
renderedUrl: renderedUrl,
|
||||
getRenderedUrl,
|
||||
instances: [instance],
|
||||
thumbnailSrc: null,
|
||||
isDerivedDisplaySet: true,
|
||||
|
||||
@ -28,15 +28,9 @@ const dicomPDFExtension = {
|
||||
* @param {object} [configuration={}]
|
||||
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
||||
*/
|
||||
getViewportModule({ servicesManager, extensionManager }) {
|
||||
getViewportModule() {
|
||||
const ExtendedOHIFCornerstonePdfViewport = props => {
|
||||
return (
|
||||
<OHIFCornerstonePdfViewport
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return <OHIFCornerstonePdfViewport {...props} />;
|
||||
};
|
||||
|
||||
return [{ name: 'dicom-pdf', component: ExtendedOHIFCornerstonePdfViewport }];
|
||||
|
||||
@ -33,14 +33,43 @@ function OHIFCornerstonePdfViewport({ displaySets, viewportId = 'pdf-viewport' }
|
||||
}
|
||||
|
||||
const { renderedUrl } = displaySets[0];
|
||||
const { getRenderedUrl } = displaySets[0];
|
||||
|
||||
useEffect(() => {
|
||||
let isCancelled = false;
|
||||
let revokeUrl;
|
||||
const abortController = new AbortController();
|
||||
|
||||
const load = async () => {
|
||||
setUrl(await renderedUrl);
|
||||
try {
|
||||
const result = getRenderedUrl
|
||||
? await getRenderedUrl({ signal: abortController.signal })
|
||||
: { url: await renderedUrl };
|
||||
|
||||
if (isCancelled) {
|
||||
result?.revoke?.();
|
||||
return;
|
||||
}
|
||||
|
||||
revokeUrl = result?.revoke;
|
||||
setUrl(result?.url || null);
|
||||
} catch (error) {
|
||||
console.warn('Failed to load PDF', error);
|
||||
if (!isCancelled) {
|
||||
setUrl(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
load();
|
||||
}, [renderedUrl]);
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
abortController.abort();
|
||||
revokeUrl?.();
|
||||
};
|
||||
}, [renderedUrl, getRenderedUrl]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -57,12 +57,16 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager)
|
||||
const { Modality, SOPInstanceUID, SeriesDescription = 'VIDEO', imageId } = instance;
|
||||
const { SeriesNumber, SeriesDate, SeriesInstanceUID, StudyInstanceUID, NumberOfFrames, url } =
|
||||
instance;
|
||||
const videoUrl = dataSource.retrieve.directURL({
|
||||
const videoUrlParams = {
|
||||
instance,
|
||||
singlepart: 'video',
|
||||
tag: 'PixelData',
|
||||
url,
|
||||
});
|
||||
};
|
||||
const videoUrl = dataSource.retrieve.directURL(videoUrlParams);
|
||||
const getVideoUrl = dataSource.retrieve.renderedURL
|
||||
? options => dataSource.retrieve.renderedURL({ ...videoUrlParams, url: videoUrl }, options)
|
||||
: undefined;
|
||||
const displaySet = {
|
||||
//plugin: id,
|
||||
Modality,
|
||||
@ -76,6 +80,8 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager)
|
||||
SOPClassHandlerId,
|
||||
referencedImages: null,
|
||||
measurements: null,
|
||||
videoUrl,
|
||||
getVideoUrl,
|
||||
viewportType: csEnums.ViewportType.VIDEO,
|
||||
instances: [instance],
|
||||
getThumbnailSrc: dataSource.retrieve.getGetThumbnailSrc?.(instance),
|
||||
|
||||
@ -1,19 +1,6 @@
|
||||
import React from 'react';
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
import { id } from './id';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneVideoViewport');
|
||||
});
|
||||
|
||||
const OHIFCornerstoneVideoViewport = props => {
|
||||
return (
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<Component {...props} />
|
||||
</React.Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@ -23,38 +10,7 @@ const dicomVideoExtension = {
|
||||
*/
|
||||
id,
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {object} [configuration={}]
|
||||
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
||||
*/
|
||||
getViewportModule({ servicesManager, extensionManager }) {
|
||||
const ExtendedOHIFCornerstoneVideoViewport = props => {
|
||||
return (
|
||||
<OHIFCornerstoneVideoViewport
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return [{ name: 'dicom-video', component: ExtendedOHIFCornerstoneVideoViewport }];
|
||||
},
|
||||
getSopClassHandlerModule,
|
||||
};
|
||||
|
||||
function _getToolAlias(toolName) {
|
||||
let toolAlias = toolName;
|
||||
|
||||
switch (toolName) {
|
||||
case 'EllipticalRoi':
|
||||
toolAlias = 'SREllipticalRoi';
|
||||
break;
|
||||
}
|
||||
|
||||
return toolAlias;
|
||||
}
|
||||
|
||||
export default dicomVideoExtension;
|
||||
|
||||
@ -1,55 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function OHIFCornerstoneVideoViewport({ displaySets }) {
|
||||
if (displaySets && displaySets.length > 1) {
|
||||
throw new Error(
|
||||
'OHIFCornerstoneVideoViewport: only one display set is supported for dicom video right now'
|
||||
);
|
||||
}
|
||||
|
||||
const { videoUrl } = displaySets[0];
|
||||
const mimeType = 'video/mp4';
|
||||
const [url, setUrl] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
setUrl(await videoUrl);
|
||||
};
|
||||
|
||||
load();
|
||||
}, [videoUrl]);
|
||||
|
||||
// Need to copies of the source to fix a firefox bug
|
||||
return (
|
||||
<div className="bg-primary-black h-full w-full">
|
||||
<video
|
||||
src={url}
|
||||
controls
|
||||
controlsList="nodownload"
|
||||
preload="auto"
|
||||
className="h-full w-full"
|
||||
crossOrigin="anonymous"
|
||||
>
|
||||
<source
|
||||
src={url}
|
||||
type={mimeType}
|
||||
/>
|
||||
<source
|
||||
src={url}
|
||||
type={mimeType}
|
||||
/>
|
||||
Video src/type not supported:{' '}
|
||||
<a href={url}>
|
||||
{url} of type {mimeType}
|
||||
</a>
|
||||
</video>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
OHIFCornerstoneVideoViewport.propTypes = {
|
||||
displaySets: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
export default OHIFCornerstoneVideoViewport;
|
||||
@ -26,7 +26,6 @@ const dicomsr = {
|
||||
|
||||
const dicomvideo = {
|
||||
sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video',
|
||||
viewport: '@ohif/extension-dicom-video.viewportModule.dicom-video',
|
||||
};
|
||||
|
||||
const dicompdf = {
|
||||
@ -141,11 +140,7 @@ function modeFactory({ modeConfiguration }) {
|
||||
viewports: [
|
||||
{
|
||||
namespace: cs3d.viewport,
|
||||
displaySetsToDisplay: [ohif.sopClassHandler],
|
||||
},
|
||||
{
|
||||
namespace: dicomvideo.viewport,
|
||||
displaySetsToDisplay: [dicomvideo.sopClassHandler],
|
||||
displaySetsToDisplay: [ohif.sopClassHandler, dicomvideo.sopClassHandler],
|
||||
},
|
||||
{
|
||||
namespace: dicompdf.viewport,
|
||||
|
||||
@ -33,7 +33,6 @@ const dicomsr = {
|
||||
|
||||
const dicomvideo = {
|
||||
sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video',
|
||||
viewport: '@ohif/extension-dicom-video.viewportModule.dicom-video',
|
||||
};
|
||||
|
||||
const dicompdf = {
|
||||
@ -254,10 +253,6 @@ function modeFactory() {
|
||||
namespace: dicomsr.viewport,
|
||||
displaySetsToDisplay: [dicomsr.sopClassHandler, dicomsr.sopClassHandler3D],
|
||||
},
|
||||
{
|
||||
namespace: dicomvideo.viewport,
|
||||
displaySetsToDisplay: [dicomvideo.sopClassHandler],
|
||||
},
|
||||
{
|
||||
namespace: dicompdf.viewport,
|
||||
displaySetsToDisplay: [dicompdf.sopClassHandler],
|
||||
|
||||
@ -44,7 +44,6 @@ export const dicomsr = {
|
||||
|
||||
export const dicomvideo = {
|
||||
sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video',
|
||||
viewport: '@ohif/extension-dicom-video.viewportModule.dicom-video',
|
||||
};
|
||||
|
||||
export const dicomecg = {
|
||||
|
||||
@ -17,7 +17,6 @@ export const cornerstone = {
|
||||
|
||||
const dicomvideo = {
|
||||
sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video',
|
||||
viewport: '@ohif/extension-dicom-video.viewportModule.dicom-video',
|
||||
};
|
||||
|
||||
const dicompdf = {
|
||||
@ -107,7 +106,7 @@ function modeFactory({ modeConfiguration }) {
|
||||
],
|
||||
},
|
||||
{
|
||||
namespace: dicomvideo.viewport,
|
||||
namespace: cornerstone.viewport,
|
||||
displaySetsToDisplay: [dicomvideo.sopClassHandler],
|
||||
},
|
||||
{
|
||||
|
||||
@ -32,7 +32,6 @@ const dicomsr = {
|
||||
|
||||
const dicomvideo = {
|
||||
sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video',
|
||||
viewport: '@ohif/extension-dicom-video.viewportModule.dicom-video',
|
||||
};
|
||||
|
||||
const dicompdf = {
|
||||
|
||||
@ -0,0 +1,74 @@
|
||||
---
|
||||
sidebar_position: 8
|
||||
sidebar_label: DICOM video viewport
|
||||
title: DICOM video viewport migration
|
||||
---
|
||||
|
||||
# DICOM video viewport migration
|
||||
|
||||
The `@ohif/extension-dicom-video` extension no longer provides its own viewport module in 3.13. DICOM video rendering is handled by the Cornerstone viewport, which supports Cornerstone3D video viewports directly.
|
||||
|
||||
The DICOM video SOP class handler remains available and should still be used to identify and build video display sets:
|
||||
|
||||
```ts
|
||||
'@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video'
|
||||
```
|
||||
|
||||
## What changed
|
||||
|
||||
The following viewport namespace has been removed:
|
||||
|
||||
```ts
|
||||
'@ohif/extension-dicom-video.viewportModule.dicom-video'
|
||||
```
|
||||
|
||||
Use the Cornerstone viewport namespace instead:
|
||||
|
||||
```ts
|
||||
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
||||
```
|
||||
|
||||
## Migration
|
||||
|
||||
If your mode has a dedicated DICOM video viewport entry, replace it with the Cornerstone viewport or add the video SOP class handler to an existing Cornerstone viewport entry.
|
||||
|
||||
**Before (3.12):**
|
||||
|
||||
```ts
|
||||
const dicomvideo = {
|
||||
sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video',
|
||||
viewport: '@ohif/extension-dicom-video.viewportModule.dicom-video',
|
||||
};
|
||||
|
||||
viewports: [
|
||||
{
|
||||
namespace: '@ohif/extension-cornerstone.viewportModule.cornerstone',
|
||||
displaySetsToDisplay: [ohif.sopClassHandler],
|
||||
},
|
||||
{
|
||||
namespace: dicomvideo.viewport,
|
||||
displaySetsToDisplay: [dicomvideo.sopClassHandler],
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
**After (3.13):**
|
||||
|
||||
```ts
|
||||
const dicomvideo = {
|
||||
sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video',
|
||||
};
|
||||
|
||||
viewports: [
|
||||
{
|
||||
namespace: '@ohif/extension-cornerstone.viewportModule.cornerstone',
|
||||
displaySetsToDisplay: [ohif.sopClassHandler, dicomvideo.sopClassHandler],
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
If your mode uses a custom viewport that wraps `OHIFCornerstoneViewport` (for example, a tracked Cornerstone viewport), route DICOM video display sets through that wrapper instead of the removed DICOM video viewport namespace.
|
||||
|
||||
## Why
|
||||
|
||||
Keeping video rendering in the Cornerstone viewport avoids a separate custom React video viewport path and keeps video behavior aligned with Cornerstone3D viewport services, tools, annotations, and presentation state handling.
|
||||
@ -23,5 +23,8 @@ The largest changes in 3.13 are infrastructure-level:
|
||||
is now **24**.
|
||||
- **[SegmentationService](./segmentation-service.md)** — the
|
||||
`removeSegmentationRepresentations` method was renamed.
|
||||
- **[DICOM video viewport](./dicom-video.md)** — the
|
||||
`@ohif/extension-dicom-video.viewportModule.dicom-video` namespace was
|
||||
removed; route DICOM video display sets through the Cornerstone viewport.
|
||||
|
||||
<DocCardList />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user