src/bundle.js
import EncodeHelper from './common/helpers'
import { isArray, isInt } from './common/utils'
import AtomicInt32 from './atomic/int32'
import AtomicString from './atomic/string'
import AtomicTimetag from './atomic/timetag'
import Message from './message'
/** OSC Bundle string */
export const BUNDLE_TAG = '#bundle'
/**
* An OSC Bundle consist of a Timetag and one or many Bundle Elements.
* The elements are either OSC Messages or more OSC Bundles
*/
export default class Bundle {
/**
* Create a Bundle instance
* @param {...*} args Timetag and elements. See examples for options
*
* @example
* const bundle = new Bundle(new Date() + 500)
*
* @example
* const message = new Message('/test/path', 51.2)
* const anotherBundle = new Bundle([message], Date.now() + 1500)
*
* @example
* const message = new Message('/test/path', 51.2)
* const anotherMessage = new Message('/test/message', 'test', 12)
* const anotherBundle = new Bundle(message, anotherMessage)
*/
constructor(...args) {
/**
* @type {number} offset
* @private
*/
this.offset = 0
/** @type {AtomicTimetag} timetag */
this.timetag = new AtomicTimetag()
/** @type {array} bundleElements */
this.bundleElements = []
if (args.length > 0) {
// first argument is an Date or js timestamp (number)
if (args[0] instanceof Date || isInt(args[0])) {
this.timetag = new AtomicTimetag(args[0])
} else if (isArray(args[0])) {
// first argument is an Array of Bundle elements
args[0].forEach((item) => {
this.add(item)
})
// second argument is an Date or js timestamp (number)
if (args.length > 1 && (args[1] instanceof Date || isInt(args[1]))) {
this.timetag = new AtomicTimetag(args[1])
}
} else {
// take all arguments as Bundle elements
args.forEach((item) => {
this.add(item)
})
}
}
}
/**
* Take a JavaScript timestamp to set the Bundle's timetag
* @param {number} ms JS timestamp in milliseconds
*
* @example
* const bundle = new Bundle()
* bundle.timestamp(Date.now() + 5000) // in 5 seconds
*/
timestamp(ms) {
if (!isInt(ms)) {
throw new Error('OSC Bundle needs an integer for setting the timestamp')
}
this.timetag = new AtomicTimetag(ms)
}
/**
* Add a Message or Bundle to the list of elements
* @param {Bundle|Message} item
*/
add(item) {
if (!(item instanceof Message || item instanceof Bundle)) {
throw new Error('OSC Bundle contains only Messages and Bundles')
}
this.bundleElements.push(item)
}
/**
* Interpret the Bundle as packed binary data
* @return {Uint8Array} Packed binary data
*/
pack() {
const encoder = new EncodeHelper()
// an OSC Bundle consists of the OSC-string "#bundle"
encoder.add(new AtomicString(BUNDLE_TAG))
// followed by an OSC Time Tag
if (!this.timetag) {
this.timetag = new AtomicTimetag()
}
encoder.add(this.timetag)
// followed by zero or more OSC Bundle Elements
this.bundleElements.forEach((item) => {
encoder.add(new AtomicInt32(item.pack().byteLength))
encoder.add(item)
})
return encoder.merge()
}
/**
* Unpack binary data to read a Bundle
* @param {DataView} dataView The DataView holding the binary representation of a Bundle
* @param {number} [initialOffset=0] Offset of DataView before unpacking
* @return {number} Offset after unpacking
*/
unpack(dataView, initialOffset = 0) {
if (!(dataView instanceof DataView)) {
throw new Error('OSC Bundle expects an instance of type DataView')
}
// read the beginning bundle string
const parentHead = new AtomicString()
parentHead.unpack(dataView, initialOffset)
if (parentHead.value !== BUNDLE_TAG) {
throw new Error('OSC Bundle does not contain a valid #bundle head')
}
// read the timetag
const timetag = new AtomicTimetag()
let offset = timetag.unpack(dataView, parentHead.offset)
// read the bundle elements
this.bundleElements = []
while (offset < dataView.byteLength) {
const head = new AtomicString()
const size = new AtomicInt32()
offset = size.unpack(dataView, offset)
// check if Packet is a Bundle or a Message
let item
head.unpack(dataView, offset)
if (head.value === BUNDLE_TAG) {
item = new Bundle()
} else {
item = new Message()
}
offset = item.unpack(dataView, offset)
this.bundleElements.push(item)
}
this.offset = offset
this.timetag = timetag
return this.offset
}
}