diff --git a/extensions/cornerstone/src/commandsModule.js b/extensions/cornerstone/src/commandsModule.js
index 9f27164a0..6759b0a6b 100644
--- a/extensions/cornerstone/src/commandsModule.js
+++ b/extensions/cornerstone/src/commandsModule.js
@@ -186,7 +186,8 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
}
const viewportInfo = getEnabledElement(i);
- const hasCornerstoneContext = viewportInfo.context &&
+ if (!viewportInfo) continue;
+ const hasCornerstoneContext =
viewportInfo.context === 'ACTIVE_VIEWPORT::CORNERSTONE';
if (hasCornerstoneContext) {
diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js
index 128120900..c83fabfdf 100644
--- a/extensions/default/src/DicomWebDataSource/index.js
+++ b/extensions/default/src/DicomWebDataSource/index.js
@@ -149,6 +149,33 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
},
},
retrieve: {
+ /* Generates a URL that can be used for direct retrieve of the bulkdata */
+ directURL: (params) => {
+ const { instance, tag = "PixelData", defaultPath = "/pixeldata", defaultType = "video/mp4" } = params;
+ const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
+ // If the BulkDataURI isn't present, then assume it uses the pixeldata endpoint
+ // The standard isn't quite clear on that, but appears to be what is expected
+ const value = instance[tag];
+ const BulkDataURI =
+ (value && value.BulkDataURI) ||
+ `series/${SeriesInstanceUID}/instances/${SOPInstanceUID}${defaultPath}`;
+ const hasQuery = BulkDataURI.indexOf('?') != -1;
+ const hasAccept = BulkDataURI.indexOf('accept=') != -1;
+ const acceptUri =
+ BulkDataURI +
+ (hasAccept ? '' : (hasQuery ? '&' : '?') + `accept=${defaultType}`);
+ if (BulkDataURI.indexOf('http') == 0) return acceptUri;
+ if (BulkDataURI.indexOf('/') == 0) {
+ return wadoRoot + acceptUri;
+ }
+ if (BulkDataURI.indexOf('series/') == 0) {
+ return `${wadoRoot}/studies/${StudyInstanceUID}/${acceptUri}`;
+ }
+ if (BulkDataURI.indexOf('instances/') === 0) {
+ return `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/${acceptUri}`;
+ }
+ throw new Error('BulkDataURI in unknown format:' + BulkDataURI);
+ },
series: {
metadata: async ({
StudyInstanceUID,
diff --git a/extensions/dicom-video/.webpack/webpack.dev.js b/extensions/dicom-video/.webpack/webpack.dev.js
new file mode 100644
index 000000000..1ae308448
--- /dev/null
+++ b/extensions/dicom-video/.webpack/webpack.dev.js
@@ -0,0 +1,8 @@
+const path = require('path');
+const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
+const SRC_DIR = path.join(__dirname, '../src');
+const DIST_DIR = path.join(__dirname, '../dist');
+
+module.exports = (env, argv) => {
+ return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
+};
diff --git a/extensions/dicom-video/.webpack/webpack.prod.js b/extensions/dicom-video/.webpack/webpack.prod.js
new file mode 100644
index 000000000..61802e5cc
--- /dev/null
+++ b/extensions/dicom-video/.webpack/webpack.prod.js
@@ -0,0 +1,44 @@
+const webpack = require('webpack');
+const merge = require('webpack-merge');
+const path = require('path');
+const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
+const pkg = require('./../package.json');
+
+const ROOT_DIR = path.join(__dirname, './..');
+const SRC_DIR = path.join(__dirname, '../src');
+const DIST_DIR = path.join(__dirname, '../dist');
+
+module.exports = (env, argv) => {
+ const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
+
+ return merge(commonConfig, {
+ devtool: 'source-map',
+ stats: {
+ colors: true,
+ hash: true,
+ timings: true,
+ assets: true,
+ chunks: false,
+ chunkModules: false,
+ modules: false,
+ children: false,
+ warnings: true,
+ },
+ optimization: {
+ minimize: true,
+ sideEffects: true,
+ },
+ output: {
+ path: ROOT_DIR,
+ library: 'OHIFExtDICOMSR',
+ libraryTarget: 'umd',
+ libraryExport: 'default',
+ filename: pkg.main,
+ },
+ plugins: [
+ new webpack.optimize.LimitChunkCountPlugin({
+ maxChunks: 1,
+ }),
+ ],
+ });
+};
diff --git a/extensions/dicom-video/LICENSE b/extensions/dicom-video/LICENSE
new file mode 100644
index 000000000..19e20dd35
--- /dev/null
+++ b/extensions/dicom-video/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Open Health Imaging Foundation
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/extensions/dicom-video/README.md b/extensions/dicom-video/README.md
new file mode 100644
index 000000000..bc9d1a98b
--- /dev/null
+++ b/extensions/dicom-video/README.md
@@ -0,0 +1,15 @@
+# DICOM Video
+This extension adds support for displaying DICOM video objects in a script tag.
+The video data must currently be available as video/mp4 on the BulkDataURI that
+is provided in the DICOMweb metadata response, and the video must have one of the
+specified SOP Class UID's in order to be recognized by the SOP class handler.
+
+Those are:
+* Video Microscop Image Storage
+* Video Photographic Image Storage
+* Video Endoscopic Image Storage
+* Secondary Capture Image Storage
+* Multiframe True Color Secondary Capture Image Storage
+
+The extension is a "standard" extension in that it is installed and available
+by default.
diff --git a/extensions/dicom-video/babel.config.js b/extensions/dicom-video/babel.config.js
new file mode 100644
index 000000000..325ca2a8e
--- /dev/null
+++ b/extensions/dicom-video/babel.config.js
@@ -0,0 +1 @@
+module.exports = require('../../babel.config.js');
diff --git a/extensions/dicom-video/package.json b/extensions/dicom-video/package.json
new file mode 100644
index 000000000..bbd0ce7ff
--- /dev/null
+++ b/extensions/dicom-video/package.json
@@ -0,0 +1,49 @@
+{
+ "name": "@ohif/extension-dicom-video",
+ "version": "0.0.1",
+ "description": "OHIF extension for video display",
+ "author": "OHIF",
+ "license": "MIT",
+ "repository": "OHIF/Viewers",
+ "main": "dist/index.umd.js",
+ "module": "src/index.js",
+ "engines": {
+ "node": ">=10",
+ "npm": ">=6",
+ "yarn": ">=1.16.0"
+ },
+ "files": [
+ "dist",
+ "README.md"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "scripts": {
+ "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
+ "dev:cornerstone": "yarn run dev",
+ "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
+ "build:package": "yarn run build",
+ "start": "yarn run dev",
+ "test:unit": "jest --watchAll",
+ "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
+ },
+ "peerDependencies": {
+ "@ohif/core": "^0.50.0",
+ "@ohif/ui": "^0.50.0",
+ "cornerstone-core": "^2.6.0",
+ "cornerstone-math": "^0.1.9",
+ "cornerstone-tools": "6.0.2",
+ "cornerstone-wado-image-loader": "4.0.4",
+ "dcmjs": "0.16.1",
+ "dicom-parser": "^1.8.9",
+ "hammerjs": "^2.0.8",
+ "prop-types": "^15.6.2",
+ "react": "^17.0.2",
+ "react-cornerstone-viewport": "4.1.2"
+ },
+ "dependencies": {
+ "@babel/runtime": "7.7.6",
+ "classnames": "^2.2.6"
+ }
+}
diff --git a/extensions/dicom-video/src/getSopClassHandlerModule.js b/extensions/dicom-video/src/getSopClassHandlerModule.js
new file mode 100644
index 000000000..5e6e8dce1
--- /dev/null
+++ b/extensions/dicom-video/src/getSopClassHandlerModule.js
@@ -0,0 +1,84 @@
+import { SOPClassHandlerName, SOPClassHandlerId } from './id';
+import { utils, classes } from '@ohif/core';
+
+const { ImageSet } = classes;
+
+const SOP_CLASS_UIDS = {
+ VIDEO_MICROSCOPIC_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.77.1.2.1',
+ VIDEO_PHOTOGRAPHIC_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.77.1.4.1',
+ VIDEO_ENDOSCOPIC_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.77.1.1.1',
+ /** Need to use fallback, could be video or image */
+ SECONDARY_CAPTURE_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.7',
+ MULTIFRAME_TRUE_COLOR_SECONDARY_CAPTURE_IMAGE_STORAGE:
+ '1.2.840.10008.5.1.4.1.1.7.4',
+};
+
+const sopClassUids = Object.values(SOP_CLASS_UIDS);
+
+const SupportedTransferSyntaxes = {
+ MPEG4_AVC_264_HIGH_PROFILE: '1.2.840.10008.1.2.4.102',
+ MPEG4_AVC_264_BD_COMPATIBLE_HIGH_PROFILE: '1.2.840.10008.1.2.4.103',
+ MPEG4_AVC_264_HIGH_PROFILE_FOR_2D_VIDEO: '1.2.840.10008.1.2.4.104',
+ MPEG4_AVC_264_HIGH_PROFILE_FOR_3D_VIDEO: '1.2.840.10008.1.2.4.105',
+ MPEG4_AVC_264_STEREO_HIGH_PROFILE: '1.2.840.10008.1.2.4.106',
+ HEVC_265_MAIN_PROFILE: '1.2.840.10008.1.2.4.107',
+ HEVC_265_MAIN_10_PROFILE: '1.2.840.10008.1.2.4.108',
+};
+
+const supportedTransferSyntaxUIDs = Object.values(SupportedTransferSyntaxes);
+
+const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager) => {
+ const dataSource = extensionManager.getActiveDataSource()[0];
+ return instances
+ .filter(metadata => {
+ const tsuid = metadata.AvailableTransferSyntaxUID ||
+ metadata.TransferSyntaxUID || metadata['00083002'];
+ return supportedTransferSyntaxUIDs.includes(tsuid);
+ })
+ .map(instance => {
+ const { Modality, FrameOfReferenceUID, SOPInstanceUID } = instance;
+ const { SeriesDescription, ContentDate, ContentTime } = instance;
+ const { SeriesNumber, SeriesDate, SeriesInstanceUID, StudyInstanceUID, NumberOfFrames } = instance;
+ const displaySet = {
+ //plugin: id,
+ Modality,
+ displaySetInstanceUID: utils.guid(),
+ SeriesDescription,
+ SeriesNumber,
+ SeriesDate,
+ SOPInstanceUID,
+ SeriesInstanceUID,
+ StudyInstanceUID,
+ SOPClassHandlerId,
+ referencedImages: null,
+ measurements: null,
+ videoUrl: dataSource.retrieve.directURL({ instance }),
+ others: [instance],
+ thumbnailSrc: dataSource.retrieve.directURL({ instance, defaultPath: "/thumbnail", defaultType: "image/jpeg", tag: "Absent" }),
+ isDerivedDisplaySet: true,
+ isLoaded: false,
+ sopClassUids,
+ numImageFrames: NumberOfFrames,
+ instance,
+ };
+ return displaySet;
+ });
+};
+
+export default function getSopClassHandlerModule({ servicesManager, extensionManager }) {
+ const getDisplaySetsFromSeries = instances => {
+ return _getDisplaySetsFromSeries(
+ instances,
+ servicesManager,
+ extensionManager
+ );
+ };
+
+ return [
+ {
+ name: SOPClassHandlerName,
+ sopClassUids,
+ getDisplaySetsFromSeries,
+ },
+ ];
+}
diff --git a/extensions/dicom-video/src/id.js b/extensions/dicom-video/src/id.js
new file mode 100644
index 000000000..6aa663316
--- /dev/null
+++ b/extensions/dicom-video/src/id.js
@@ -0,0 +1,7 @@
+const id = 'org.ohif.dicom-video';
+
+export default id;
+
+const SOPClassHandlerName = 'dicom-video';
+const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`;
+export { SOPClassHandlerName, SOPClassHandlerId };
diff --git a/extensions/dicom-video/src/index.js b/extensions/dicom-video/src/index.js
new file mode 100644
index 000000000..fb563ab40
--- /dev/null
+++ b/extensions/dicom-video/src/index.js
@@ -0,0 +1,99 @@
+import React from 'react';
+import getSopClassHandlerModule from './getSopClassHandlerModule';
+import id from './id.js';
+
+const Component = React.lazy(() => {
+ return import(
+ /* webpackPrefetch: true */ './viewports/OHIFCornerstoneVideoViewport'
+ );
+});
+
+const OHIFCornerstoneVideoViewport = props => {
+ return (
+