From 75f438b1a333e3fb6cb59aedc893cac4b5d6e2f6 Mon Sep 17 00:00:00 2001 From: Richard Cartwright Date: Thu, 16 May 2019 13:13:29 +0100 Subject: [PATCH] feat: minimal viable playback control for quantel servers --- README.md | 225 ++++++++++++++++++++++++++++++++++++++ src/cxx/orbital.cc | 267 +++++++++++++++++++++++++++++++++------------ src/index.ts | 6 + 3 files changed, 426 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 5e6a289..460a6a8 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,228 @@ This library uses native bindings to bridge the Quantel ISA System CORBA API and ## Dev install instructions TBD. Note that in its current form, this library will only build on Windows and targets Win32 (`ia32`) architecture. A 32-bit version of node is required to run it. + +### Prerequisites + +* This addon has been developed with Node.js v8.1.15 LTS and makes use of the N-API which is not available in earlier versions than 8. +* Ensure the build system has the [node-gyp prerequisites](https://github.com/nodejs/node-gyp#installation). +* For development, install the `node-gyp` build tool globally with `npm install -g node-gyp`. +* Either install the Quantel ISA dummy server installation or ensure you have access to an installed ISA and sQ servers. (Not ones that are on-air ... yet!) +* Install the [yarn package manager](https://yarnpkg.com/en/docs/instal). + +### Building + +Install packages and build the native extension: + + yarn install + +Build the typescript interface: + + yarn build + +## Play walkthrough + +### Importing + +Experiment from the REPL with: + + const { Quantel } = require('.') + +Import into an external project with: + + import { Quantel } from 'tv-automation-quantel-gateway' + const { Quantel } = require('tv-autonation-quantel-gateway') + +### Server Connection + +If the ISA is running on the localhost on the default port, each of the calls will establish connection automatically. Otherwise, it is necessary to estabinsh the ISA IOR reference with a call to: + +```Javascript +await Quantel.getISAReference('http://isa.adresss.or.name:port/') +``` + +This requests the CORBA reference to the ISA over HTTP. + +Test the CORBA connection with: + +```Javascript +Quantel.testConnection().then(console.log) +// true +``` + +### Playout walkthrough + +The following example assumes you are using the Node.js REPL. As all methods return promises, in a project it is recommended that `async`/`await` are used. + +#### Record a clip + +This following steps assume that you have already stored clips onto servers. With the dummy servers, the _Controller UI_ can be used to do this. Select the _System_ tab, click on a server (e.g. _1100_), double click on a channel (e.g. _S1100C2_), put the port into record mode, select number of frames to record (default _1000_), click _Initial Frames_ then click _Start_. Once recording is complete, click _Save_. Once finished, click _Release_ to release the port. + +#### Create a playout port + +An single ISA software system controls a _zone_. To find out details of the current zone: + +```Javascript +Quantel.getZoneInfo().then(console.log) +``` + +```Javascript +{ type: 'ZonePortal', + zoneNumber: 1000, + zoneName: 'Dummy Zone 1000' } +``` + +Each zone has some hardware _servers_. Each _server_ has a number of _channels_ which, in practice, map to the SDI ports on the device. Servers also have _pools_ of disk, a sort of mount point for a RAID of hard disks. To find out about the servers connected to an ISA, call `getServers`. + +```Javascript +Quantel.getServers().then(console.log) +``` + +```Javascript +[ { type: 'Server', + ident: 1100, + down: false, + name: 'Dummy 1100', + numChannels: 4, + pools: [ 11 ], + portNames: [ 'solitary' ] }, + { type: 'Server', + ident: 1200, + down: false, + name: 'Dummy 1200', + numChannels: 2, + pools: [ 12 ], + portNames: [] }, + { type: 'Server', + ident: 1300, + down: false, + name: 'Dummy 1300', + numChannels: 3, + pools: [ 13 ], + portNames: [] } ] +``` + +To be able to play out material on a channel of a specific server, it is necessary to create a logical playout port attached to that channel and configured for playout. + +```Javascript +Quantel.createPlayPort({ serverID: 1100, portName: 'nrk8', channelNo: 1}).then(console.log) +``` + +```Javascript +{ type: 'PortInfo', + serverID: 1100, + channelNo: 1, + audioOnly: false, + portName: 'nrk8', + portID: 1, + assigned: true } +``` + +Note that the `assigned` flag is `true` only if the port is newly created, otherwise it is set to `false`. Also note that it is very easy to steal someone else's port! + +#### Find fragments + +To be able to play a clip, the fragments that make up that clip must be loaded onto a port. The details of what fragments are required and where they are stored are available in the database attached to ISA. To query the fragments for a clip with ID `2`: + +```Javascript +let frags +Quantel.getAllFragments({ clipID: 2 }).then(f => { frags = f; console.log(f); }) +``` + +```Javascript +{ type: 'ServerFramgments', + clipID: 2, + fragments: + [ { type: 'VideoFragment', + trackNum: 0, + start: 0, + finish: 1000, + rushID: '344aed5ed1204908a54302de951eecb7', + format: 90, + poolID: 11, + poolFrame: 5, + skew: 0, + rushFrame: 0 }, + { type: 'EffectFragment', /* ... */ }, + { type: 'EffectFragment', /* ... */ }, + { type: 'AudioFragment', + trackNum: 0, + start: 0, + finish: 1000, + rushID: '520c2157fc66443b9e2fc580cb2cf789', + format: 73, + poolID: 11, + poolFrame: 8960, + skew: 0, + rushFrame: 0 } ] } +``` + +#### Load fragments onto port + +To load fragments onto a port, the server with the port must also have the disk storage `pool` where the fragments are stored attached. Otherwise a clone will need to be initiated (to follow). Loading the clips at offset 0 in the timeline of the port: + +```Javascript +Quantel.loadPlayPort({ serverID: 1100, portName: 'nrk8', offset: 0, fragments: frags }) +``` + +#### Check port status + +At any time after the creation of a port and before its release, it is possible to check its status: + +```Javascript +Quantel.getPlayPortStatus({ serverID: 1100, portName: 'nrk8'}).then(console.log) +``` + +```Javascript +{ type: 'PortStatus', + serverID: 1100, + portName: 'nrk8', + portID: 1, + speed: 1, + offset: 0, + status: 'readyToPlay', + endOfData: 1000, + framesUnused: 0 } +``` + +The status message includes the current position of the play head (`offset`) measured in frames and the speed that the media is play at (float value, `0.5` for half speed, `-1.0` for reverse etc..). (To follow: setting speed?) + +#### Control playout + +To start and stop playback, use triggers. For example: + +```Javascript +Quantel.trigger({ serverID: 1100, portName: 'nrk8', trigger: Quantel.Trigger.START }) +Quantel.trigger({ serverID: 1100, portName: 'nrk8', trigger: Quantel.Trigger.STOP }) +``` + +It is also possible to set an offset at which play should stop, e.g.: + +```Javascript +Quantel.trigger({ serverID: 1100, portName: 'nrk8', + trigger: Quantel.Trigger.STOP, offset: 300 }) +``` + +To jump to a specific frame, ideally when stopped and not when playing out live, use `jump`: + +```Javascript +Quantel.jump({ serverID: 1100, portName: 'nrk8', offset: 123 }) +``` + +Note that if the port is playing when asked to jump, it will jump and stop. + +To follow: get `setJump` to work. + +#### Release the port + +Once playout is finished, release the port and its resources with: + +```Javascript +Quantel.releasePort({ serverID: 1100, portName: 'nrk8'}) +``` + +## License + +Unless otherwise called out in the header of a file, the files of this project are licensed under the GPL V2 or later, as detailed in the [`LICENSE`](./LICENSE) file. This project links to [omniORB](http://omniorb.sourceforge.net/) that is similarly GPL v2. + +Please also note the specific terms of the [`Quentin.idl`](./include/quantel/Quentin.idl) file. diff --git a/src/cxx/orbital.cc b/src/cxx/orbital.cc index ac0b0ce..6e378b1 100644 --- a/src/cxx/orbital.cc +++ b/src/cxx/orbital.cc @@ -341,6 +341,9 @@ napi_value createPlayPort(napi_env env, napi_callback_info info) { CHECK_STATUS; status = napi_get_value_bool(env, prop, &audioOnly); CHECK_STATUS; + } else { + status = napi_get_value_bool(env, false, &prop); + CHECK_STATUS; } status = napi_set_named_property(env, result, "audioOnly", prop); CHECK_STATUS; @@ -520,15 +523,27 @@ napi_value getPlayPortStatus(napi_env env, napi_callback_info info) { CHECK_STATUS; break; case 2: - case 3: status = napi_create_string_utf8(env, "playing", NAPI_AUTO_LENGTH, &prop); CHECK_STATUS; break; + case 3: + status = napi_create_string_utf8(env, "playing&readyToPlay", NAPI_AUTO_LENGTH, &prop); + CHECK_STATUS; + break; case 4: + status = napi_create_string_utf8(env, "jumpReady", NAPI_AUTO_LENGTH, &prop); + CHECK_STATUS; + break; case 5: + status = napi_create_string_utf8(env, "jumpReady&readyToPlay", NAPI_AUTO_LENGTH, &prop); + CHECK_STATUS; + break; case 6: + status = napi_create_string_utf8(env, "jumpReady&playing", NAPI_AUTO_LENGTH, &prop); + CHECK_STATUS; + break; case 7: - status = napi_create_string_utf8(env, "jumpReady", NAPI_AUTO_LENGTH, &prop); + status = napi_create_string_utf8(env, "jumpReady&readyToPlay&playing", NAPI_AUTO_LENGTH, &prop); CHECK_STATUS; break; case 8: @@ -924,19 +939,6 @@ napi_value getAllFragments(napi_env env, napi_callback_info info) { } status = napi_set_named_property(env, result, "fragments", prop); CHECK_STATUS; - - /* Quentin::Server_ptr server = zp->getServer(1100); - Quentin::Port_ptr port = server->getPort(L"solitary", 0); - - fragments[1] = fragments[3]; - fragments->length(2); - port->load(0, fragments); - - printf("Action at trigger result START %i\n", port->actionAtTrigger(START, Quentin::Port::trActStart)); - port->actionAtTrigger(STOP, Quentin::Port::trActStop); - - printf("Trying to start now %i\n", port->setTrigger(START, Quentin::Port::trModeNow, 1)); - port->setTrigger(STOP, Quentin::Port::trModeOffset, 300); */ } catch(CORBA::SystemException& ex) { NAPI_THROW_CORBA_EXCEPTION(ex); @@ -966,6 +968,9 @@ napi_value loadPlayPort(napi_env env, napi_callback_info info) { Quentin::WStrings_var portNames; Quentin::ServerFragments* fragments; char rushID[33]; + char typeName[32]; + uint32_t fragmentNo; + uint32_t fragmentCount = 0; try { status = retrieveZonePortal(env, info, &orb, &zp); @@ -1016,74 +1021,95 @@ napi_value loadPlayPort(napi_env env, napi_callback_info info) { CHECK_STATUS; Quentin::ServerFragments fragments; - fragments.length(1); - status = napi_get_element(env, fragprop, 0, &prop); + status = napi_get_array_length(env, fragprop, &fragmentNo); CHECK_STATUS; + fragments.length(fragmentNo); - // FIXME support more than just the video fragment + for ( int i = 0 ; i < fragmentNo ; i++ ) { + status = napi_get_element(env, fragprop, i, &prop); + CHECK_STATUS; - Quentin::PositionData vfd = {}; + Quentin::ServerFragment sf = {}; + Quentin::PositionData vfd = {}; + Quentin::ServerFragmentData sfd; + std::string rushIDStr; - status = napi_get_named_property(env, prop, "format", &subprop); - CHECK_STATUS; - status = napi_get_value_int32(env, subprop, (int32_t *) &vfd.format); - CHECK_STATUS; + status = napi_get_named_property(env, prop, "trackNum", &subprop); + CHECK_STATUS; + status = napi_get_value_int32(env, subprop, (int32_t *) &sf.trackNum); + CHECK_STATUS; - status = napi_get_named_property(env, prop, "poolID", &subprop); - CHECK_STATUS; - status = napi_get_value_int32(env, subprop, (int32_t *) &vfd.poolID); - CHECK_STATUS; + status = napi_get_named_property(env, prop, "start", &subprop); + CHECK_STATUS; + status = napi_get_value_int32(env, subprop, (int32_t *) &sf.start); + CHECK_STATUS; - status = napi_get_named_property(env, prop, "poolFrame", &subprop); - CHECK_STATUS; - status = napi_get_value_int32(env, subprop, (int32_t *) &vfd.poolFrame); - CHECK_STATUS; + status = napi_get_named_property(env, prop, "finish", &subprop); + CHECK_STATUS; + status = napi_get_value_int32(env, subprop, (int32_t *) &sf.finish); + CHECK_STATUS; - status = napi_get_named_property(env, prop, "skew", &subprop); - CHECK_STATUS; - status = napi_get_value_int32(env, subprop, (int32_t *) &vfd.skew); - CHECK_STATUS; + status = napi_get_named_property(env, prop, "type", &subprop); + CHECK_STATUS; + status = napi_get_value_string_utf8(env, subprop, typeName, 32, nullptr); + CHECK_STATUS; - status = napi_get_named_property(env, prop, "rushFrame", &subprop); - CHECK_STATUS; - status = napi_get_value_int64(env, subprop, (int64_t *) &vfd.rushFrame); - CHECK_STATUS; + printf("Processing fragment of type: %s\n", typeName); - status = napi_get_named_property(env, prop, "rushID", &subprop); - CHECK_STATUS; - status = napi_get_value_string_utf8(env, subprop, rushID, 33, nullptr); - CHECK_STATUS; - std::string rushIDStr(rushID); - printf("Left %s and right %s\n", rushIDStr.substr(0, 16).c_str(), rushIDStr.substr(16, 32).c_str()); - vfd.rushID = { - (CORBA::LongLong) strtoull(rushIDStr.substr(0, 16).c_str(), nullptr, 16), - (CORBA::LongLong) strtoull(rushIDStr.substr(16, 32).c_str(), nullptr, 16) }; - vfd.rushFrame = 0; + switch (typeName[0]) { + case 'V': // VideoFragment + case 'A': // AudioFragment & AUXFragment - printf("Checking poolFrame = %i rushFrame = %i skew = %i\n", vfd.poolFrame, vfd.rushFrame, vfd.skew); + status = napi_get_named_property(env, prop, "format", &subprop); + CHECK_STATUS; + status = napi_get_value_int32(env, subprop, (int32_t *) &vfd.format); + CHECK_STATUS; - Quentin::ServerFragmentData sfd; - sfd.videoFragmentData(vfd); - Quentin::ServerFragment sf = {}; - sf.fragmentData = sfd; + status = napi_get_named_property(env, prop, "poolID", &subprop); + CHECK_STATUS; + status = napi_get_value_int32(env, subprop, (int32_t *) &vfd.poolID); + CHECK_STATUS; - status = napi_get_named_property(env, prop, "trackNum", &subprop); - CHECK_STATUS; - status = napi_get_value_int32(env, subprop, (int32_t *) &sf.trackNum); - CHECK_STATUS; + status = napi_get_named_property(env, prop, "poolFrame", &subprop); + CHECK_STATUS; + status = napi_get_value_int32(env, subprop, (int32_t *) &vfd.poolFrame); + CHECK_STATUS; - status = napi_get_named_property(env, prop, "start", &subprop); - CHECK_STATUS; - status = napi_get_value_int32(env, subprop, (int32_t *) &sf.start); - CHECK_STATUS; + status = napi_get_named_property(env, prop, "skew", &subprop); + CHECK_STATUS; + status = napi_get_value_int32(env, subprop, (int32_t *) &vfd.skew); + CHECK_STATUS; - status = napi_get_named_property(env, prop, "finish", &subprop); - CHECK_STATUS; - status = napi_get_value_int32(env, subprop, (int32_t *) &sf.finish); - CHECK_STATUS; + status = napi_get_named_property(env, prop, "rushFrame", &subprop); + CHECK_STATUS; + status = napi_get_value_int64(env, subprop, (int64_t *) &vfd.rushFrame); + CHECK_STATUS; - printf("trackNum = %i start = %i end = %i\n", sf.trackNum, sf.start, sf.finish); - fragments[0] = sf; + status = napi_get_named_property(env, prop, "rushID", &subprop); + CHECK_STATUS; + status = napi_get_value_string_utf8(env, subprop, rushID, 33, nullptr); + CHECK_STATUS; + rushIDStr.assign(rushID); + vfd.rushID = { + (CORBA::LongLong) strtoull(rushIDStr.substr(0, 16).c_str(), nullptr, 16), + (CORBA::LongLong) strtoull(rushIDStr.substr(16, 32).c_str(), nullptr, 16) }; + vfd.rushFrame = 0; + + if (typeName[0] == 'V') { + sfd.videoFragmentData(vfd); + } else if (typeName[1] == 'u') { // AudioFragment + sfd.audioFragmentData(vfd); + } else { + sfd.auxFragmentData(vfd); + } + sf.fragmentData = sfd; + fragments[fragmentCount++] = sf; + break; + default: + break; + } // switch typeName[0] + } // for loop through incoming fragments + fragments.length(fragmentCount); Quentin::Server_ptr server = zp->getServer(serverID); @@ -1229,6 +1255,102 @@ napi_value trigger(napi_env env, napi_callback_info info) { return prop; } +napi_value qJump(napi_env env, napi_callback_info info) { + napi_status status; + napi_value prop, options; + napi_valuetype type; + bool isArray; + CORBA::ORB_var orb; + Quentin::ZonePortal::_ptr_type zp; + int32_t serverID; + int32_t offset = 0; + char* portName; + size_t portNameLen; + Quentin::WStrings_var portNames; + + try { + status = retrieveZonePortal(env, info, &orb, &zp); + CHECK_STATUS; + + size_t argc = 2; + napi_value argv[2]; + status = napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); + CHECK_STATUS; + + if (argc < 2) { + NAPI_THROW_ORB_DESTROY("Options object with server ID, port name and jump offset must be provided."); + } + status = napi_typeof(env, argv[1], &type); + CHECK_STATUS; + status = napi_is_array(env, argv[1], &isArray); + CHECK_STATUS; + if (isArray || type != napi_object) { + NAPI_THROW_ORB_DESTROY("Argument must be an options object with server ID, port name and jump offset."); + } + + options = argv[1]; + status = napi_get_named_property(env, options, "serverID", &prop); + CHECK_STATUS; + status = napi_get_value_int32(env, prop, &serverID); + CHECK_STATUS; + + status = napi_get_named_property(env, options, "portName", &prop); + CHECK_STATUS; + status = napi_get_value_string_utf8(env, prop, nullptr, 0, &portNameLen); + CHECK_STATUS; + portName = (char*) malloc((portNameLen + 1) * sizeof(char)); + status = napi_get_value_string_utf8(env, prop, portName, portNameLen + 1, &portNameLen); + CHECK_STATUS; + + status = napi_get_named_property(env, options, "offset", &prop); + CHECK_STATUS; + status = napi_typeof(env, prop, &type); + CHECK_STATUS; + if (type == napi_number) { + status = napi_get_value_int32(env, prop, &offset); + CHECK_STATUS; + } + + Quentin::Server_ptr server = zp->getServer(serverID); + + std::wstring_convert> utf8_conv; + std::wstring wportName = utf8_conv.from_bytes(portName); + + // Prevent accidental creation of extra port + portNames = server->getPortNames(); + bool foundPort = false; + for ( int x = 0 ; x < portNames->length() ; x++ ) { + if (wcscmp(wportName.data(), (const wchar_t *) portNames[x]) == 0) { + foundPort = true; + break; + } + } + free(portName); + if (!foundPort) { + NAPI_THROW_ORB_DESTROY("Cannot set jump point on a port with an unknown port name."); + } + + Quentin::Port_ptr port = server->getPort(utf8_conv.from_bytes(portName).data(), 0); + + // Disable preload is documented as not implemented + port->jump(offset, false); + } + catch(CORBA::SystemException& ex) { + NAPI_THROW_CORBA_EXCEPTION(ex); + } + catch(CORBA::Exception& ex) { + NAPI_THROW_CORBA_EXCEPTION(ex); + } + catch(omniORB::fatalException& fe) { + NAPI_THROW_FATAL_EXCEPTION(fe); + } + + orb->destroy(); + status = napi_get_boolean(env, true, &prop); + CHECK_STATUS; + return prop; +} + napi_value setJump(napi_env env, napi_callback_info info) { napi_status status; napi_value prop, options; @@ -1252,14 +1374,14 @@ napi_value setJump(napi_env env, napi_callback_info info) { CHECK_STATUS; if (argc < 2) { - NAPI_THROW_ORB_DESTROY("Options object with server ID, port name and action must be provided."); + NAPI_THROW_ORB_DESTROY("Options object with server ID, port name and jump offset must be provided."); } status = napi_typeof(env, argv[1], &type); CHECK_STATUS; status = napi_is_array(env, argv[1], &isArray); CHECK_STATUS; if (isArray || type != napi_object) { - NAPI_THROW_ORB_DESTROY("Argument must be an options object with server ID, port name and action."); + NAPI_THROW_ORB_DESTROY("Argument must be an options object with server ID, port name and jump offset."); } options = argv[1]; @@ -1306,7 +1428,7 @@ napi_value setJump(napi_env env, napi_callback_info info) { Quentin::Port_ptr port = server->getPort(utf8_conv.from_bytes(portName).data(), 0); - port->jump(offset, true); + port->setJump(offset); } catch(CORBA::SystemException& ex) { NAPI_THROW_CORBA_EXCEPTION(ex); @@ -1342,13 +1464,14 @@ napi_value Init(napi_env env, napi_value exports) { DECLARE_NAPI_METHOD("getAllFragments", getAllFragments), DECLARE_NAPI_METHOD("loadPlayPort", loadPlayPort), DECLARE_NAPI_METHOD("trigger", trigger), + DECLARE_NAPI_METHOD("jump", qJump), DECLARE_NAPI_METHOD("setJump", setJump), { "START", nullptr, nullptr, nullptr, nullptr, start, napi_enumerable, nullptr }, { "STOP", nullptr, nullptr, nullptr, nullptr, stop, napi_enumerable, nullptr }, { "JUMP", nullptr, nullptr, nullptr, nullptr, jump, napi_enumerable, nullptr }, { "TRANSITION", nullptr, nullptr, nullptr, nullptr, transition, napi_enumerable, nullptr }, }; - status = napi_define_properties(env, exports, 14, desc); + status = napi_define_properties(env, exports, 15, desc); return exports; } diff --git a/src/index.ts b/src/index.ts index bfef518..3c2b130 100644 --- a/src/index.ts +++ b/src/index.ts @@ -121,6 +121,7 @@ export namespace Quantel { } export async function testConnection (): Promise { + if (!isaIOR) await getISAReference() return quantel.testConnection(await isaIOR) } @@ -164,6 +165,11 @@ export namespace Quantel { return quantel.trigger(await isaIOR, options) } + export async function jump (options: JumpInfo): Promise { + if (!isaIOR) await getISAReference() + return quantel.jump(await isaIOR, options) + } + export async function setJump (options: JumpInfo): Promise { if (!isaIOR) await getISAReference() return quantel.setJump(await isaIOR, options)