Skip to content

Additional source-filter testing #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Install SDPoker as a dependency for the project you are working on:
Use the module in your project with the following line:

```javascript
const { getSDP, checkRFC4566, checkST2110 } = require('sdpoker');
const { getSDP, checkRFC4566, checkRFC4570, checkST2110 } = require('sdpoker');
```

### Get SDP
Expand All @@ -56,7 +56,7 @@ getSDP('http://localhost:3123/sdps/video_stream_1.sdp')

If the `nmos` flag is set to `true`, the SDP file is required to be retrieved over HTTP and must have filename extension `.sdp`.

The value of a fulfilled promise is the contents of an SDP file as a string. SDP files are assumed to be UTF8 character sets. Pass the result into the `checkRFC4566` and `checkST2110` methods.
The value of a fulfilled promise is the contents of an SDP file as a string. SDP files are assumed to be UTF8 character sets. Pass the result into the `checkRFC4566`, `checkRFC4570` and `checkST2110` methods.

### Check RFC4566

Expand All @@ -75,6 +75,23 @@ The `params` parameter is an object that, when present, can be used to configure

The return value of the method is an array of [Javascript Errors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error). The array is empty if no errors occurred.

### Check RFC4570

The `checkRFC4570(sdp, params)` takes a string representation of the contents of an SDP file (`sdp`) and runs source-filter tests relevant to SMPTE ST 2110.

For example:

```javascript
getSDP('examples/st2110-10.sdp')
.then(sdp => checkRFC4570(sdp, {}))
.then(errs => { if (errs.length > 0) console.log(errs); })
.catch(console.error);
```

The `params` parameter is an object that, when present, can be used to configure the tests. See the [parameters](#parameters) section below for more information.

The return value of the method is an array of [Javascript Errors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error). The array is empty if no errors occurred.

### Check ST2110

The `checkST2110(sdp, params)` takes a string representation of the contents of an SDP file (`sdp`) and runs through the relevant clauses of the SMPTE 2110-10/20/30 documents, and referenced standards such as AES-67 and SMPTE ST 2022-7, applying appropriate tests.
Expand Down Expand Up @@ -123,18 +140,17 @@ let params = {
};
```

Currently, the `whitespace` flag forces a check as to whether the format parameter field (`a=fmtp`) has a whitespace character after the final semicolon on the line. Strict reading of the standard suggests that it should, although the this could also be viewed as ambiguous as the term _carriage return_ can also be interpreted as whitespace. Further white space checks may be added, such as should a space be included between `a=source-filter:` and `incl`.
Currently, the `whitespace` flag forces a check as to whether the format parameter field (`a=fmtp`) has a whitespace character after the final semicolon on the line. Strict reading of the standard suggests that it should, although the this could also be viewed as ambiguous as the term _carriage return_ can also be interpreted as whitespace.

# Tests

For now, please see the comments in files `checkRFC4566.js` and `checkST2110.js` for a description of the tests. A more formal and separate list may be provided in the future.
For now, please see the comments in files `checkRFC4566.js`, `checkRFC4570.js` and `checkST2110.js` for a description of the tests. A more formal and separate list may be provided in the future.

# Enhancements

The following items are known deficiencies of SDPoker and may be added in the future:

* Tests for attribute `a=recvonly`
* Tests for attribute `a=sourcefilter`
* Testing whether an advertised connection address can be resolved, joined or pinged.
* Testing whether advertised clocks are available.
* Testing that, for AES-67 audio streams, the `ptime` attribute matches the sample rate and number of channels.
Expand Down
115 changes: 115 additions & 0 deletions checkRFC4570.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/* Copyright 2018 Streampunk Media Ltd.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

const splitLines = sdp => sdp.match(/[^\r\n]+/g);
const concat = arrays => Array.prototype.concat.apply([], arrays);

const sourceFilterPattern = /a=source-filter:\s(incl|excl)\sIN\s(IP4|IP6|\*)\s(\S+)(?:\s(\S+))+/;
const cPattern = /^c=IN\s+(IP[46])\s+([^\s/]+)(\/\d+)?(\/[1-9]\d*)?$/;
const multiPattern = /^((22[4-9]|23[0-9])(\.(\d\d?\d?)){3})|(ff[0-7][123458e]::[^\s]+)$/;

// Section 3 Test 1 - Source-filter correctly formatted if present
const test_30_1 = sdp => {
let lines = splitLines(sdp);
let errors = [];
for ( let x = 0 ; x < lines.length ; x++ ) {
if (lines[x].startsWith('a=source-filter:')) {
let sourceFilterMatch = lines[x].match(sourceFilterPattern);
if (!sourceFilterMatch) {
errors.push(new Error(`Line ${x + 1}: Source-filters must follow the pattern 'a=source-filter: <filter-mode> <filter-spec>' as per RFC 4570 Section 3.`));
continue;
}
}
}
return errors;
};

// Section 3 Test 2 - Source-filters are present when multicast addresses are used
const test_30_2 = sdp => {
let lines = splitLines(sdp);
let errors = [];
let isMulticast = false;
let seenSourceFilter = false;
for ( let x = 0 ; x < lines.length ; x++ ) {
if (lines[x].startsWith('c=')) {
let addrMatch = lines[x].match(cPattern);
if (!addrMatch) {
continue;
}
if (multiPattern.test(addrMatch[2])) {
isMulticast = true;
}
}
if (lines[x].startsWith('a=source-filter:')) {
seenSourceFilter = true;
}
}
if (isMulticast && !seenSourceFilter) {
// A basic check that at least one source-filter is included when using a multicast destination
errors.push(new Error('SDP file includes one or more multicast destinations but does not include any a=source-filter lines as per RFC 4570 Section 3.'));
}
return errors;
};

// Section 3.0 Test 3 - Source-filters match addresses used in connection attributes
const test_30_3 = sdp => {
let lines = splitLines(sdp);
let errors = [];
let globalAddr = null;
let mediaAddr = null;
let inMedia = false;
for ( let x = 0 ; x < lines.length ; x++ ) {
if (lines[x].startsWith('c=')) {
let addrMatch = lines[x].match(cPattern);
if (!addrMatch) {
continue;
}
if (!inMedia) {
globalAddr = addrMatch[2];
} else {
mediaAddr = addrMatch[2];
}
}
if (lines[x].startsWith('m=')) {
inMedia = true;
mediaAddr = null;
}
if (lines[x].startsWith('a=source-filter:')) {
let sourceFilterMatch = lines[x].match(sourceFilterPattern);
if (!sourceFilterMatch) {
continue;
}
if (sourceFilterMatch[3] != globalAddr && sourceFilterMatch[3] != mediaAddr && sourceFilterMatch[3] != '*') {
errors.push(new Error(`Line ${x + 1}: Source-filter destination addresses must match one or more connection address as per RFC 4570 Section 3.`));
}
}
}
return errors;
};

const section_30 = (sdp, params) => {
let tests = [ test_30_1, test_30_2, test_30_3 ];
return concat(tests.map(t => t(sdp, params)));
};

const allSections = (sdp, params) => {
let sections = [ section_30 ];
return concat(sections.map(s => s(sdp, params)));
};

module.exports = {
allSections,
section_30
};
19 changes: 1 addition & 18 deletions checkST2110.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ const concat = arrays => Array.prototype.concat.apply([], arrays);
const mediaclkPattern = /[\r\n]a=mediaclk/;
const mediaclkTypePattern = /[\r\n]a=mediaclk[^\s=]+/g;
const mediaclkDirectPattern = /[\r\n]a=mediaclk:direct=\d+\s+/g;
const sourceFilterPattern = /a=source-filter:\s(incl|excl)/;
const tsrefclkPattern = /[\r\n]a=ts-refclk/;
const ptpPattern = /traceable|((([0-9a-fA-F]{2}-){7}[0-9a-fA-F]{2})(:(\d+|domain-name=\S+))?)/;
const macPattern = /(([0-9a-fA-F]{2}-){5}[0-9a-fA-F]{2})/;
Expand Down Expand Up @@ -110,22 +109,6 @@ const test_10_81_2 = (sdp, params) => {
}
};

// Test ST 2110-10 Section 8.1 Test 3 - Source-filter correctly formatted if present
const test_10_81_3 = sdp => {
let lines = splitLines(sdp);
let errors = [];
for ( let x = 0 ; x < lines.length ; x++ ) {
if (lines[x].startsWith('a=source-filter:')) {
let sourceFilterMatch = lines[x].match(sourceFilterPattern);
if (!sourceFilterMatch) {
errors.push(new Error(`Line ${x + 1}: Source-filters must follow the pattern 'a=source-filter: <filter-mode> <filter-spec>' as defined in RFC 4570.`));
continue;
}
}
}
return errors;
};

// Test ST2110-10 Section 8.1 Test 1 - Shall have a media-level ts-refclk
const test_10_82_1 = sdp => {
let errors = [];
Expand Down Expand Up @@ -1097,7 +1080,7 @@ const section_10_74 = (sdp, params) => {
};

const section_10_81 = (sdp, params) => {
let tests = [ test_10_81_1, test_10_81_2, test_10_81_3 ];
let tests = [ test_10_81_1, test_10_81_2 ];
return concat(tests.map(t => t(sdp, params)));
};

Expand Down
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
const { allSections : checkRFC4566 } = require('./checkRFC4566.js');
const { allSections : checkRFC4570 } = require('./checkRFC4570.js');
const { allSections : checkST2110 } = require('./checkST2110.js');

const getSDP = (path, nmos = true) => {
Expand All @@ -42,5 +43,6 @@ const getSDP = (path, nmos = true) => {
module.exports = {
getSDP,
checkRFC4566,
checkRFC4570,
checkST2110
};
7 changes: 4 additions & 3 deletions sdpoker.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
limitations under the License.
*/

const { getSDP, checkRFC4566, checkST2110 } = require('./index.js');
const { getSDP, checkRFC4566, checkRFC4570, checkST2110 } = require('./index.js');
const yargs = require('yargs');
const { accessSync, R_OK } = require('fs');

Expand Down Expand Up @@ -78,9 +78,10 @@ const args = yargs
async function test (args) {
try {
let sdp = await getSDP(args._[0], args.nmos);
let rfcErrors = checkRFC4566(sdp, args);
let rfc4566Errors = checkRFC4566(sdp, args);
let rfc4570Errors = checkRFC4570(sdp, args);
let st2110Errors = checkST2110(sdp, args);
let errors = rfcErrors.concat(st2110Errors);
let errors = rfc4566Errors.concat(rfc4570Errors, st2110Errors);
if (errors.length !== 0) {
console.error(`Found ${errors.length} error(s) in SDP file:`);
for ( let c in errors ) {
Expand Down