UNPKG

dji_srt_parser

Version:

Parses and interprets DJI's drones SRT metadata

363 lines (340 loc) 12 kB
//Export to Adobe After Effect's mgJSON format. It's poorly documented, but here's a minimal working example: https://github.com/JuanIrache/mgjson const padStringNumber = require('./padStringNumber'); const bigStr = require('./bigStr'); const deduceHeaders = require('./deduceHeaders'); //After Effects can't read larger numbers const largestMGJSONNum = 2147483648; //Build the style that After Effects needs for static text function createDataOutlineChildText(matchName, displayName, value) { if (typeof value != 'string') value = value.toString(); return { objectType: 'dataStatic', displayName, dataType: { type: 'string', paddedStringProperties: { maxLen: value.length, maxDigitsInStrLength: value.length.toString().length, eventMarkerB: false } }, matchName, value }; } //Choose best value for altitude function chooseAlt(pckt, elevationOffset) { let alt = 0; if (pckt.ALTITUDE != undefined) { alt = pckt.ALTITUDE; } else if (pckt.BAROMETER != undefined) { alt = pckt.BAROMETER; } else if (pckt.HB != undefined) { alt = pckt.HB; } else if (pckt.HS != undefined) { alt = pckt.HS; } return elevationOffset + alt; } //Build the style that After Effects needs for dynamic values: numbers, arrays of numbers (axes) or strings (date) function createDynamicDataOutline(matchName, displayName, units, type) { let result = { objectType: 'dataDynamic', displayName, sampleSetID: matchName, dataType: { type }, //We apply (linear) interpolation to numeric values only interpolation: type === 'paddedString' ? 'hold' : 'linear', hasExpectedFrequecyB: false, //Some values will be set afterwards sampleCount: null, matchName }; if (units) result.displayName += ` [${units}]`; if (type === 'numberString') { //Number saved as string (After Effects reasons) //Add fourCC to help AE identify streams result.dataType.numberStringProperties = { pattern: { //Will be calculated later digitsInteger: 0, digitsDecimal: 0, //Will use plus and minus signs always. Seems easier isSigned: true }, range: { //We use the allowed extremes, will compare to actual data occuring: { min: largestMGJSONNum, max: -largestMGJSONNum }, //Legal values could potentially be modified per stream type (for example, latitude within -+85, longitude -+180... but what's the benefit?) legal: { min: -largestMGJSONNum, max: largestMGJSONNum } } }; } else if (type === 'numberStringArray') { //Array of numbers, for example axes of a sensor let deducedHeaders = deduceHeaders({ name: displayName, units }); //Set datatype result.dataType.numberArrayProperties = { pattern: { isSigned: true, digitsInteger: 0, digitsDecimal: 0 }, //Limited to 3 axes, we split the rest to additional streams arraySize: units.length, //Set tentative headers for each array. arrayDisplayNames: deducedHeaders, arrayRanges: { ranges: units.map(s => ({ occuring: { min: largestMGJSONNum, max: -largestMGJSONNum }, legal: { min: -largestMGJSONNum, max: largestMGJSONNum } })) } }; } else if (type === 'paddedString') { //Any other value is expressed as string //Add fourCC to help AE identify streams result.dataType.paddedStringProperties = { maxLen: 0, maxDigitsInStrLength: 0, eventMarkerB: false }; } return result; } //Returns the data as parts of an mgjson object function convertSamples(data, elevationOffset) { //Will hold the description of each stream let dataOutline = []; //Holds the streams let dataDynamicSamples = []; //Start deducing streams here function addOneStream({ streamName, units, sampleSetID, type, extract }) { try { if (data && data.length && extract(data[0]) != null) { //Prepare sample set let sampleSet = { sampleSetID, samples: [] }; //Create the stream structure let dataOutlineChild = createDynamicDataOutline( sampleSetID, streamName, units, type ); //And find the type const setMaxMinPadStr = function (val, outline) { //Set found max lengths outline.dataType.paddedStringProperties.maxLen = Math.max( val.toString().length, outline.dataType.paddedStringProperties.maxLen ); outline.dataType.paddedStringProperties.maxDigitsInStrLength = Math.max( val.length.toString().length, outline.dataType.paddedStringProperties.maxDigitsInStrLength ); }; //Loop all the samples data.forEach(s => { //Extract wanted data const value = extract(s); //Update mins and maxes const setMaxMinPadNum = function (val, pattern, range) { range.occuring.min = Math.min(val, range.occuring.min); range.occuring.max = Math.max(val, range.occuring.max); // Copy occuring min and max to legal ones. This usually avoids a bug in AE where it mixes up float and int values and limits ranges incorrectly range.legal.min = range.occuring.min; range.legal.max = range.occuring.max; //And max left and right padding pattern.digitsInteger = Math.max( bigStr(Math.floor(val)).length, pattern.digitsInteger ); pattern.digitsDecimal = Math.max( bigStr(val).replace(/^\d*\.?/, '').length, pattern.digitsDecimal ); }; //Back to data samples. Check that at least we have the valid values if (value != null) { let sample = { time: new Date(s.DATE) }; if (type === 'numberString') { //Save numbers as strings sample.value = bigStr(value); //Update mins, maxes and padding setMaxMinPadNum( value, dataOutlineChild.dataType.numberStringProperties.pattern, dataOutlineChild.dataType.numberStringProperties.range ); } else if (type === 'numberStringArray') { //Save arrays of numbers as arrays of strings sample.value = []; value.forEach((v, i) => { sample.value[i] = bigStr(v); //And update, mins, maxs and paddings setMaxMinPadNum( v, dataOutlineChild.dataType.numberArrayProperties.pattern, dataOutlineChild.dataType.numberArrayProperties.arrayRanges .ranges[i] ); }); } else if (type === 'paddedString') { //Save anything else as (padded)string sample.value = { length: value.length.toString(), str: value }; setMaxMinPadStr(value, dataOutlineChild); } //Save sample sampleSet.samples.push(sample); } }); sampleSet.samples.forEach(s => { if (type === 'numberString') { //Apply max padding to every sample s.value = padStringNumber( s.value, dataOutlineChild.dataType.numberStringProperties.pattern .digitsInteger, dataOutlineChild.dataType.numberStringProperties.pattern .digitsDecimal ); } else if (type === 'numberStringArray') { //Apply max padding to every sample s.value = s.value.map(v => padStringNumber( v, dataOutlineChild.dataType.numberArrayProperties.pattern .digitsInteger, dataOutlineChild.dataType.numberArrayProperties.pattern .digitsDecimal ) ); } else if (type === 'paddedString') { //Apply max padding to every sample s.value.str = s.value.str.padEnd( dataOutlineChild.dataType.paddedStringProperties.maxLen, ' ' ); s.value.length = s.value.length.padStart( dataOutlineChild.dataType.paddedStringProperties .maxDigitsInStrLength, '0' ); } }); //Save total samples count dataOutlineChild.sampleCount = sampleSet.samples.length; //Save stream dataOutline.push(dataOutlineChild); dataDynamicSamples.push(sampleSet); } } catch (error) { console.error(error); } } //Add all the data we are interested in if (data && data.length) { addOneStream({ streamName: 'GPS: (Lat.,Long.,Alt.)', units: ['deg', 'deg', 'm'], sampleSetID: `streamGPS`, type: 'numberStringArray', extract: function (s) { return [s.GPS.LATITUDE, s.GPS.LONGITUDE, chooseAlt(s, elevationOffset)]; } }); addOneStream({ streamName: 'SPEED: (2D,3D,Vert.)', units: ['km/h', 'km/h', 'km/h'], sampleSetID: `streamSPEED`, type: 'numberStringArray', extract: function (s) { return [s.SPEED.TWOD, s.SPEED.THREED, s.SPEED.VERTICAL]; } }); addOneStream({ streamName: 'DISTANCE:', units: ['m'], sampleSetID: `streamDISTANCE`, type: 'numberString', extract: function (s) { return s.DISTANCE; } }); addOneStream({ streamName: 'ISO:', units: null, sampleSetID: `streamISO`, type: 'numberString', extract: function (s) { return s.ISO; } }); addOneStream({ streamName: 'SHUTTER:', units: null, sampleSetID: `streamSHUTTER`, type: 'numberString', extract: function (s) { return s.SHUTTER; } }); addOneStream({ streamName: 'FNUM:', units: null, sampleSetID: `streamFNUM`, type: 'numberString', extract: function (s) { return s.FNUM; } }); addOneStream({ streamName: 'DATE:', units: null, sampleSetID: `streamDATE`, type: 'paddedString', extract: function (s) { return new Date(s.DATE).toISOString(); } }); } return { dataOutline, dataDynamicSamples }; } //Converts the processed data to After Effects format module.exports = function (data, name = '', elevationOffset) { const converted = convertSamples(data, elevationOffset); //The format is very convoluted. This is the outer structure let result = { version: 'MGJSON2.0.0', creator: 'https://github.com/JuanIrache/DJI_SRT_Parser', dynamicSamplesPresentB: true, dynamicDataInfo: { useTimecodeB: false, utcInfo: { precisionLength: 3, isGMT: true } }, //Create first data point with filename dataOutline: [ createDataOutlineChildText( 'filename', 'File name', name.replace(/\.srt/gi, '') // Global, for concatenated filenames ), ...converted.dataOutline ], //And paste the converted data dataDynamicSamples: converted.dataDynamicSamples }; //Remove dynamic data if no samples if (!result.dataDynamicSamples.length) { delete result.dataDynamicSamples; delete result.dynamicDataInfo; result.dynamicSamplesPresentB = false; } return result; };