@irbisadm/simple-sdp 0.1.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/dist/index.mjs +1 -1
- package/package.json +67 -43
- package/readme.md +188 -23
- package/types/index.d.ts +293 -259
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The project
|
|
4
|
+
follows [Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## [1.0.0] — unreleased
|
|
7
|
+
|
|
8
|
+
The model of 0.2.0 is now the stable 1.x API. No breaking changes since 0.2.0.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `sdpWriter.write(sdp): string`. Lines are written in RFC 8866 §5 order with CRLF;
|
|
12
|
+
attribute order inside a section is normalized (see README, "Writing"). Round-trip
|
|
13
|
+
tests cover every fixture.
|
|
14
|
+
- `sdpValidator.validate(sdp | text, {type?}): SdpIssue[]`, plus types `SdpIssue`,
|
|
15
|
+
`SdpIssueCode` and `SdpValidateOptions` (rules: README, "Validation").
|
|
16
|
+
- `package.json`: an `exports` map with `types` and `sideEffects: false`.
|
|
17
|
+
|
|
18
|
+
## [0.2.0] — unreleased
|
|
19
|
+
|
|
20
|
+
Parser rewrite: strict RFC 8866, a lossless model, typed WebRTC attributes. **Breaking.**
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- Time unit `h` in `r=`/`t=`/`z=` is 3600 s, not 86400 s (RFC 8866 §5.10).
|
|
24
|
+
- `a=sendonly`, `a=recvonly` and `a=inactive` were not recognized as directions
|
|
25
|
+
(only `sendrecv` was).
|
|
26
|
+
- `r=`: active duration was read from the repeat-interval field, and offsets were dropped
|
|
27
|
+
unless the line had more than three fields.
|
|
28
|
+
- `b=`: `bwtype` contained the `b=` prefix (`"b=AS"`).
|
|
29
|
+
- `u=` overwrote `i=` instead of being stored as `uri`.
|
|
30
|
+
- Media-level `i=`, `b=` and `k=` overwrote the session-level fields.
|
|
31
|
+
- `a=rtcp-fb` kept only its first token (`nack pli` → `nack`).
|
|
32
|
+
- `a=group` with semantics other than the seven built-in ones was silently dropped.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- `SdpParseError` (`lineNumber`, `line`, `reason`). It is thrown for malformed lines,
|
|
36
|
+
unknown line types, line order violations (RFC 8866 §5), missing `v=`/`o=`/`s=`/`t=`
|
|
37
|
+
and malformed typed attributes.
|
|
38
|
+
- `z=` (time zone adjustments), multiple `b=`/`e=`/`p=` lines, LF-only line endings.
|
|
39
|
+
- Typed attributes:
|
|
40
|
+
`a=candidate` (RFC 8839), `a=end-of-candidates` (RFC 8840), `a=ice-lite`,
|
|
41
|
+
`a=ssrc` / `a=ssrc-group` (RFC 5576), `a=msid` (RFC 8830), `a=rid` (RFC 8851),
|
|
42
|
+
`a=simulcast` (RFC 8853), `a=rtcp-mux` (RFC 5761), `a=rtcp-rsize` (RFC 5506),
|
|
43
|
+
`a=extmap-allow-mixed` (RFC 8285), `a=rtcp` (RFC 3605), `a=sctp-port` and
|
|
44
|
+
`a=max-message-size` (RFC 8841). The `a=extmap` direction and extension attributes
|
|
45
|
+
are now kept as well.
|
|
46
|
+
- A known attribute outside its scope (e.g. `a=mid` at session level), or `rtpmap`/`fmtp`/`rtcp-fb`
|
|
47
|
+
for a payload type missing from `m=`, is kept in `attributes` instead of being lost.
|
|
48
|
+
- Test fixtures: real SDP from Chrome 156, Firefox 159, Chrome (2023) and SIP devices.
|
|
49
|
+
|
|
50
|
+
### Changed (migration from 0.1.x)
|
|
51
|
+
|
|
52
|
+
| 0.1.x | 0.2.0 |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `protocolVersion?`, `origin?`, `sessionName?`, `timing?`, `media?` | required (always present after parsing) |
|
|
55
|
+
| `origin.sessVersion: number` | `string` (like `sessId`; 64-bit values exist) |
|
|
56
|
+
| `origin.netType: 'IN'`, `addrType: 'IP4' \| 'IP6'` (others became `undefined`) | `string`, kept verbatim |
|
|
57
|
+
| `repeat[]` (session level) | `timing[i].repeat[]` (belongs to its `t=`) |
|
|
58
|
+
| `emailAddress`, `phoneNumber` | `emailAddresses[]`, `phoneNumbers[]` |
|
|
59
|
+
| `encryptionKeys[]` | `encryptionKey` (at most one per level) |
|
|
60
|
+
| `bandwidthInformation` | `bandwidths[]` |
|
|
61
|
+
| `ice.ufrag` / `ice.pwd` / `ice.options` | `iceUfrag` / `icePwd` / `iceOptions` |
|
|
62
|
+
| `dtls.algorithm` + `dtls.fingerprint` | `fingerprints[]: {hashFunction, fingerprint}` |
|
|
63
|
+
| `dtls.setup` (defaulted to `actpass` when absent) | `setup` (absent when absent) |
|
|
64
|
+
| `group[].token` | `group[].semantics` |
|
|
65
|
+
| `media[].ports: number[]` | `port`, `numberOfPorts?` |
|
|
66
|
+
| `media[].fmt[]` (`contentType: 'opus/48000/2'`, `formatParameters`) | `codecs[]` (`encodingName`, `clockRate`, `encodingParameters`, `parameters`, `rtcpFeedback`) |
|
|
67
|
+
| fmtp parameter without value → `'unknown'` | `null` |
|
|
68
|
+
| `media[].fmtp[]` / `media[].description` (non-RTP) | `formats[]` |
|
|
69
|
+
| `extmap: Record<number, string>` | `extmap: SdpExtmap[]` |
|
|
70
|
+
| `msid?: {streamId, trackId}` | `msid?: SdpMsid[]` |
|
|
71
|
+
| `attributes: Record<string, string \| true \| string[]>` | `attributes: {name, value?}[]` in original order |
|
|
72
|
+
| types `SdpAVMediaSection`, `SdpAppMediaSection`, `SdpOtherMediaSection` | `SdpMediaSection` |
|
|
73
|
+
| types `SdpAttribute`, `SdpIceAttributes`, `SdpDtlsAttribute` | `SdpGenericAttribute`, flat fields |
|
|
74
|
+
| invalid input sometimes produced `NaN` or partial objects | `SdpParseError` |
|
|
75
|
+
|
|
76
|
+
## [0.1.1] — 2023-03-20
|
|
77
|
+
|
|
78
|
+
- Initial public release: `sdpReader.parse`, `sdpReader.isValid`.
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const e=(e,t)=>e.replace(`${t}=`,"").split(" "),t=/^\d+$/,r=e=>{if(!t.test(e))throw new Error(`"${e}" is not an integer.`);return parseInt(e)},s=e=>{const t={d:86400,h:86400,m:60,s:1};if(Object.keys(t).includes(e.slice(-1))){const s=e.slice(-1),n=e.slice(0,e.length-1),a=r(n);return t[s]*a}return r(e)},n=e=>e.split("/").map((e=>parseInt(e))),a=t=>{const r=e(t,"m");switch(r[0]){case"audio":return(e=>{const t={type:e[0],ports:n(e[1]),proto:e[2]};if(e.length>3){t.fmtp=[];for(let r=3;r<e.length;r++)t.fmtp.push(parseInt(e[r]))}return t})(r);case"application":return(e=>({type:"application",ports:n(e[1]),proto:e[2],description:e[3]}))(r);default:return(e=>{const t={type:e[0],ports:n(e[1]),proto:e[2]};if(e.length>3){t.fmtp=[];for(let r=3;r<e.length;r++)t.fmtp.push(e[r])}return t})(r)}},i=e=>{const t=e.replace("k=",""),[s,n]=t.split(":");return{bwtype:s,bandwidth:r(n)}},o=t=>{const r=e(t,"c");return{netType:"IN"===r[0]?"IN":void 0,addrType:["IP4","IP6"].includes(r[1])?r[1]:void 0,connectionAddress:r[2]}},p=(e,t,r)=>{e.ice||(e.ice={ufrag:"",pwd:"",options:[]}),e.ice[t]=r},c=(e,t,r)=>{e.dtls||(e.dtls={algorithm:"",fingerprint:"",setup:"actpass"}),e.dtls[t]=r},l=(e,t)=>{e.attributes||(e.attributes={});const r=t.replace("a=",""),[s,...n]=r.split(":"),a=n.join(":");switch(s){case"mid":e.mid=a;break;case"ice-ufrag":p(e,"ufrag",a);break;case"ice-pwd":p(e,"pwd",a);break;case"ice-options":p(e,"options",a.split(" "));break;case"fingerprint":{const[t,r]=a.split(" ");c(e,"algorithm",t),c(e,"fingerprint",r);break}case"setup":c(e,"setup",a);break;case"sendrecv":e.direction=s;break;case"group":((e,t)=>{e.group||(e.group=[]);const[r,...s]=t.split(" ");["LS","FID","SRF","ANAT","FEC","DDP","BUNDLE"].includes(r)&&e.group.push({token:r,mids:s})})(e,a);break;default:if(e.attributes[s]){Array.isArray(e.attributes[s])||(e.attributes[s]=[e.attributes[s]]),e.attributes[s].push(a);break}e.attributes[s]=0===n.length||a}return e},u=t=>{const r=t.split("\r\n"),n={};let p=null;return r.forEach(((t,c)=>{if(0!==t.length){if("="!==t[1])throw new Error(`Invalid line structure in line "${t}"`);switch(t[0]){case"v":n.protocolVersion=(e=>parseInt(e.replace("v=","")))(t);break;case"o":n.origin=(t=>{const r=e(t,"o"),s=parseInt(r[2]),n="IN"===r[3]?"IN":void 0,a=["IP4","IP6"].includes(r[4])?r[4]:void 0;return{username:r[0],sessId:r[1],sessVersion:s,netType:n,addrType:a,unicastAddress:r[5]}})(t);break;case"s":n.sessionName=(e=>e.replace("s=",""))(t);break;case"t":n.timing||(n.timing=[]),n.timing.push((t=>{const r=e(t,"t"),s=parseInt(r[0]),n=isNaN(s)?0:s,a=parseInt(r[1]);return{startTime:n,stopTime:isNaN(a)?0:a}})(t));break;case"r":n.repeat||(n.repeat=[]),n.repeat.push((t=>{const r=e(t,"r"),n={repeatInterval:s(r[0]),activeDuration:s(r[0])};if(r.length>3){n.offsets=[];for(let e=2;e<r.length;e++)n.offsets.push(s(r[e]))}return n})(t));break;case"i":n.sessionInformation=(e=>e.replace("i=",""))(t);break;case"u":n.sessionInformation=(e=>e.replace("u=",""))(t);break;case"e":n.emailAddress=(e=>e.replace("e=",""))(t);break;case"p":n.phoneNumber=(e=>e.replace("p=",""))(t);break;case"k":n.encryptionKeys||(n.encryptionKeys=[]),n.encryptionKeys.push((e=>{const t=e.replace("k=",""),[r,s]=t.split(":");if(!["clear","base64","uri","prompt"].includes(r))throw new Error(`Encryption method "${r}" is not supported by RFC-4566`);return{method:r,encryptionKey:s}})(t));break;case"m":const r=a(t);p=r,n.media||(n.media=[]),n.media.push(r);break;case"b":p?p.bandwidthInformation=i(t):n.bandwidthInformation=i(t);break;case"c":p?p.connectionInformation=o(t):n.connectionInformation=o(t);break;case"a":l(p||n,t);break;default:throw Error(`Unknown line type "${t[0]}"`)}}else if(c!==r.length-1)throw new Error(`Empty line at "${c}" after ${r[c-1]}`)})),n},d={parse:u,isValid:(e,t=!1)=>{try{return u(e),!0}catch(e){return t&&console.error(e),!1}}},b="0.1.1";export{d as sdpReader,b as version};
|
|
1
|
+
class e extends Error{constructor(e,t,r){super(t>0?`SDP line ${t} "${r}": ${e}`:`SDP: ${e}`),this.name="SdpParseError",this.reason=e,this.lineNumber=t,this.line=r}}const t=/^\d+$/,r=/^\d+$/,i=(e,t="value")=>{if(!r.test(e))throw new Error(`${t} "${e}" is not a non-negative integer`);return parseInt(e,10)},s={d:86400,h:3600,m:60,s:1},n=(e,t=!1)=>{if(t&&e.startsWith("-"))return-n(e.slice(1));const r=e.slice(-1);return r in s?s[r]*i(e.slice(0,-1),"time"):i(e,"time")},o=e=>{const t=e.indexOf(":"),r=-1===t?e:e.slice(0,t);if(0===r.length)throw new Error("empty key method");return-1===t?{method:r}:{method:r,encryptionKey:e.slice(t+1)}},a=e=>/(^|\/)RTP\//.test(e),d=e=>{const t=e.indexOf(":");if(t<=0)throw new Error('expected "<bwtype>:<bandwidth>"');return{bwtype:e.slice(0,t),bandwidth:i(e.slice(t+1),"bandwidth")}},c=e=>{const t=e.split(" ");if(3!==t.length)throw new Error('expected "<nettype> <addrtype> <connection-address>"');const[r,i,s]=t;return{netType:r,addrType:i,connectionAddress:s}},p=(e,t)=>{if(void 0===e||0===e.length)throw new Error(`expected a value: ${t}`);return e},l=e=>{if(void 0!==e)throw new Error("flag attribute must not have a value");return!0},m=e=>{const t=e.indexOf(" ");return-1===t?[e,void 0]:[e.slice(0,t),e.slice(t+1)]},u=(e,t)=>{var r;const s=i(t,"payload type");return null===(r=e.codecs)||void 0===r?void 0:r.find((e=>e.payloadType===s))},f=(e,t,r,i)=>{if(void 0!==e[t])throw new Error(`duplicate a=${i}`);e[t]=r},v=(e,t,r)=>{void 0===e[t]&&(e[t]=[]),e[t].push(r)},h=["sendrecv","sendonly","recvonly","inactive"],g=e=>{const t={};for(const r of e.split(";")){const e=r.trim();if(0===e.length)continue;const i=e.indexOf("=");-1===i?t[e]=null:t[e.slice(0,i)]=e.slice(i+1)}return t},w='"<foundation> <component> <transport> <priority> <address> <port> typ <type> [raddr <addr>] [rport <port>] *(<ext-name> <ext-value>)"',$=e=>{const t=i(e,"ssrc-id");if(t>4294967295)throw new Error(`ssrc-id ${t} does not fit into 32 bits`);return t},y=/^[A-Za-z0-9_-]+$/,b=["session","media"],x=["media"],k=["session"],E=["active","passive","actpass","holdconn"],T=e=>({scopes:b,parse:t=>f(t,"direction",e,e)}),j={sendrecv:T("sendrecv"),sendonly:T("sendonly"),recvonly:T("recvonly"),inactive:T("inactive"),"ice-ufrag":{scopes:b,parse:(e,t)=>f(e,"iceUfrag",p(t,"a=ice-ufrag:<ufrag>"),"ice-ufrag")},"ice-pwd":{scopes:b,parse:(e,t)=>f(e,"icePwd",p(t,"a=ice-pwd:<password>"),"ice-pwd")},"ice-options":{scopes:b,parse:(e,t)=>f(e,"iceOptions",p(t,"a=ice-options:<option> ...").split(" "),"ice-options")},"ice-lite":{scopes:k,parse:(e,t)=>f(e,"iceLite",l(t),"ice-lite")},candidate:{scopes:x,parse:(e,t)=>v(e,"candidates",(e=>{var t;const r=e.split(" ");if(r.length<8||"typ"!==r[6]||r.length%2!=0)throw new Error(`expected ${w}`);const[s,n,o,a,d,c,,p]=r,l={foundation:s,component:i(n,"component id"),transport:o,priority:i(a,"priority"),address:d,port:i(c,"port"),type:p};for(let e=8;e<r.length;e+=2){const[s,n]=[r[e],r[e+1]];if(0===s.length||0===n.length)throw new Error(`expected ${w}`);switch(s){case"raddr":l.relatedAddress=n;break;case"rport":l.relatedPort=i(n,"rport");break;case"tcptype":l.tcpType=n;break;default:(null!==(t=l.extensions)&&void 0!==t?t:l.extensions={})[s]=n}}return l})(p(t,"a=candidate:<candidate>")))},"end-of-candidates":{scopes:b,parse:(e,t)=>f(e,"endOfCandidates",l(t),"end-of-candidates")},fingerprint:{scopes:b,parse:(e,t)=>{const[r,i]=m(p(t,"a=fingerprint:<hash-function> <fingerprint>"));if(!i||i.includes(" "))throw new Error('expected "a=fingerprint:<hash-function> <fingerprint>"');v(e,"fingerprints",{hashFunction:r,fingerprint:i})}},setup:{scopes:b,parse:(e,t)=>{if(!E.includes(t))throw new Error(`a=setup must be one of ${E.join(", ")}`);f(e,"setup",t,"setup")}},extmap:{scopes:b,parse:(e,t)=>v(e,"extmap",(e=>{const[t,r]=m(e);if(!r)throw new Error('expected "<id>[/<direction>] <uri> [<attributes>]"');const[s,n,...o]=t.split("/");if(o.length||void 0!==n&&!h.includes(n))throw new Error(`invalid extmap id "${t}"`);const[a,d]=m(r),c={id:i(s,"extmap id"),uri:a};return void 0!==n&&(c.direction=n),void 0!==d&&(c.attributes=d),c})(p(t,"a=extmap:<id> <uri>")))},group:{scopes:k,parse:(e,t)=>v(e,"group",(e=>{const[t,...r]=e.split(" ");if(0===t.length||r.some((e=>0===e.length)))throw new Error('expected "<semantics> <mid> ..."');return{semantics:t,mids:r}})(p(t,"a=group:<semantics> <mid> ...")))},mid:{scopes:x,parse:(e,t)=>f(e,"mid",p(t,"a=mid:<identification-tag>"),"mid")},msid:{scopes:x,parse:(e,t)=>v(e,"msid",(e=>{const[t,r,...i]=e.split(" ");if(0===t.length||""===r||i.length)throw new Error('expected "<stream id> [<track id>]"');return void 0===r?{streamId:t}:{streamId:t,trackId:r}})(p(t,"a=msid:<stream id> [<track id>]")))},ssrc:{scopes:x,parse:(e,t)=>((e,t)=>{var r;const[i,s]=m(t);if(!s)throw new Error('expected "<ssrc-id> <attribute>[:<value>]"');const n=$(i),o=s.indexOf(":"),a=-1===o?s:s.slice(0,o),d=-1===o?null:s.slice(o+1),c=null!==(r=e.ssrcs)&&void 0!==r?r:e.ssrcs=[];let p=c.find((e=>e.id===n));return p||(p={id:n,attributes:{}},c.push(p)),!(a in p.attributes)&&(p.attributes[a]=d,!0)})(e,p(t,"a=ssrc:<ssrc-id> <attribute>[:<value>]"))},"ssrc-group":{scopes:x,parse:(e,t)=>v(e,"ssrcGroups",(e=>{const[t,...r]=e.split(" ");if(0===t.length)throw new Error('expected "<semantics> <ssrc-id> ..."');return{semantics:t,ssrcs:r.map($)}})(p(t,"a=ssrc-group:<semantics> <ssrc-id> ...")))},rid:{scopes:x,parse:(e,t)=>v(e,"rids",(e=>{const[t,r]=m(e),[s,n]=r?m(r):[void 0,void 0];if(!y.test(t)||"send"!==s&&"recv"!==s||""===n)throw new Error('expected "<rid-id> <send|recv> [<params>]"');const o={id:t,direction:s};if(void 0!==n){const e=g(n);if("pt"in e){if(!n.startsWith("pt=")||null===e.pt)throw new Error('"pt=" must be the first rid parameter');o.payloadTypes=e.pt.split(",").map((e=>i(e,"payload type"))),delete e.pt}Object.keys(e).length&&(o.restrictions=e)}return o})(p(t,"a=rid:<rid-id> <send|recv>")))},simulcast:{scopes:x,parse:(e,t)=>f(e,"simulcast",(e=>{const t=e.split(" ");if(2!==t.length&&4!==t.length)throw new Error('expected "send|recv <streams> [send|recv <streams>]"');const r={};for(let e=0;e<t.length;e+=2){const i=t[e];if("send"!==i&&"recv"!==i||r[i])throw new Error('expected "send|recv <streams> [send|recv <streams>]"');r[i]=t[e+1].split(";").map((e=>e.split(",").map((e=>{const t=e.startsWith("~"),r=t?e.slice(1):e;if(!y.test(r))throw new Error(`invalid simulcast rid "${e}"`);return t?{rid:r,paused:t}:{rid:r}}))))}return r})(p(t,"a=simulcast:<send|recv> <streams>")),"simulcast")},"extmap-allow-mixed":{scopes:b,parse:(e,t)=>f(e,"extmapAllowMixed",l(t),"extmap-allow-mixed")},"rtcp-mux":{scopes:x,parse:(e,t)=>f(e,"rtcpMux",l(t),"rtcp-mux")},"rtcp-rsize":{scopes:x,parse:(e,t)=>f(e,"rtcpRsize",l(t),"rtcp-rsize")},rtcp:{scopes:x,parse:(e,t)=>{const r=p(t,"a=rtcp:<port> [<nettype> <addrtype> <address>]").split(" ");if(1!==r.length&&4!==r.length)throw new Error('expected "a=rtcp:<port> [<nettype> <addrtype> <address>]"');const[s,n,o,a]=r;f(e,"rtcp",1===r.length?{port:i(s,"port")}:{port:i(s,"port"),netType:n,addrType:o,address:a},"rtcp")}},"sctp-port":{scopes:x,parse:(e,t)=>f(e,"sctpPort",i(p(t,"a=sctp-port:<port>"),"sctp-port"),"sctp-port")},"max-message-size":{scopes:x,parse:(e,t)=>f(e,"maxMessageSize",i(p(t,"a=max-message-size:<size>"),"max-message-size"),"max-message-size")},rtpmap:{scopes:x,parse:(e,t)=>{const[r,s]=m(p(t,"a=rtpmap:<payload type> <encoding>")),n=e.codecs?u(e,r):void 0;if(!n||void 0!==n.encodingName)return!1;((e,t)=>{const[r,s,n,...o]=t.split("/");if(!r||void 0===s||o.length)throw new Error('expected "<payload type> <encoding name>/<clock rate>[/<encoding parameters>]"');e.encodingName=r,e.clockRate=i(s,"clock rate"),void 0!==n&&(e.encodingParameters=i(n,"encoding parameters"))})(n,null!=s?s:"")}},fmtp:{scopes:x,parse:(e,t)=>{const[r,i]=m(p(t,"a=fmtp:<format> <parameters>")),s=e.codecs?u(e,r):void 0;if(!s||void 0!==s.parameters||void 0===i)return!1;s.parameters=g(i)}},"rtcp-fb":{scopes:x,parse:(e,t)=>{const[r,i]=m(p(t,"a=rtcp-fb:<payload type> <feedback>"));if(!i||!e.codecs)return!1;if("*"===r)return void v(e,"rtcpFeedbackWildcard",i);const s=u(e,r);if(!s)return!1;v(s,"rtcpFeedback",i)}}},P=(e,t,r)=>{const i=r.indexOf(":"),s=-1===i?r:r.slice(0,i),n=-1===i?void 0:r.slice(i+1);if(0===s.length)throw new Error("empty attribute name");const o=j[s];o&&o.scopes.includes(t)&&!1!==o.parse(e,n)||v(e,"attributes",void 0===n?{name:s}:{name:s,value:n})},z={v:0,o:1,s:2,i:3,u:4,e:5,p:6,c:7,b:8,t:9,r:10,z:11,k:12,a:13},I=new Set(["e","p","b","a"]),F={m:0,i:1,c:2,b:3,k:4,a:5},O=new Set(["b","a"]),R={session:"v o s i u e p c b t r z k a",media:"m i c b k a"},N=r=>{const s=r.split(/\r?\n/);""===s[s.length-1]&&s.pop();const p={media:[]};let l=null,m=null,u=-1;s.forEach(((r,s)=>{var f,v,h,g,w,$,y;const b=t=>{throw new e(t,s+1,r)};0===r.length&&b("empty line"),(r.length<2||"="!==r[1])&&b('expected "<type>=<value>"');const x=r[0],k=r.slice(2);if(0===s&&"v"!==x&&b('a session description must start with "v="'),"m"===x)u=0;else{const e=l?"media":"session",t=l?O:I,r=(l?F:z)[x];void 0===r&&b(x in z?`"${x}=" is not allowed in a media description`:`unknown line type "${x}"`),l||"t"!==x?l||"r"!==x&&"z"!==x?r<u?b(`line is out of order (${e} order: ${R[e]})`):r!==u||t.has(x)||b(l&&"c"===x?'multiple "c=" lines in one media description are not supported':`duplicate "${x}=" line`):u!==z.t&&u!==z.r&&b(`"${x}=" must follow "t=" or "r="`):u>z.z&&b(`"t=" must precede "k=" and "a=" (order: ${R.session})`),u=r}try{if(l){switch(x){case"i":l.information=k;break;case"c":l.connectionInformation=c(k);break;case"b":(null!==(f=l.bandwidths)&&void 0!==f?f:l.bandwidths=[]).push(d(k));break;case"k":l.encryptionKey=o(k);break;case"a":P(l,"media",k)}if("m"!==x)return}switch(x){case"v":"0"!==k&&b(`unsupported protocol version "${k}"`),p.protocolVersion=0;break;case"o":p.origin=(e=>{const r=e.split(" ");if(6!==r.length)throw new Error('expected "<username> <sess-id> <sess-version> <nettype> <addrtype> <unicast-address>"');const[i,s,n,o,a,d]=r;if(!t.test(s))throw new Error(`sess-id "${s}" is not numeric`);if(!t.test(n))throw new Error(`sess-version "${n}" is not numeric`);return{username:i,sessId:s,sessVersion:n,netType:o,addrType:a,unicastAddress:d}})(k);break;case"s":p.sessionName=k;break;case"i":p.sessionInformation=k;break;case"u":p.uri=k;break;case"e":(null!==(v=p.emailAddresses)&&void 0!==v?v:p.emailAddresses=[]).push(k);break;case"p":(null!==(h=p.phoneNumbers)&&void 0!==h?h:p.phoneNumbers=[]).push(k);break;case"c":p.connectionInformation=c(k);break;case"b":(null!==(g=p.bandwidths)&&void 0!==g?g:p.bandwidths=[]).push(d(k));break;case"t":m=(e=>{const t=e.split(" ");if(2!==t.length)throw new Error('expected "<start-time> <stop-time>"');return{startTime:i(t[0],"start time"),stopTime:i(t[1],"stop time")}})(k),(null!==(w=p.timing)&&void 0!==w?w:p.timing=[]).push(m);break;case"r":(null!==($=(y=m).repeat)&&void 0!==$?$:y.repeat=[]).push((e=>{const t=e.split(" ");if(t.length<3)throw new Error('expected "<repeat interval> <active duration> <offset>..."');const[r,i,...s]=t.map((e=>n(e)));return{repeatInterval:r,activeDuration:i,offsets:s}})(k));break;case"z":m.zoneAdjustments=(e=>{const t=e.split(" ");if(t.length<2||t.length%2!=0)throw new Error('expected pairs of "<adjustment time> <offset>"');const r=[];for(let e=0;e<t.length;e+=2)r.push({adjustmentTime:i(t[e],"adjustment time"),offset:n(t[e+1],!0)});return r})(k);break;case"k":p.encryptionKey=o(k);break;case"a":P(p,"session",k);break;case"m":l=(e=>{const t=e.split(" ");if(t.length<4)throw new Error('expected "<media> <port> <proto> <fmt> ..."');const[r,s,n,...o]=t,[d,c,...p]=s.split("/");if(p.length)throw new Error(`invalid port "${s}"`);const l={type:r,port:i(d,"port"),proto:n};return void 0!==c&&(l.numberOfPorts=i(c,"number of ports")),a(n)?l.codecs=o.map((e=>{const t=i(e,"payload type");if(t>127)throw new Error(`payload type ${t} is out of range 0-127`);return{payloadType:t}})):l.formats=o,l})(k),p.media.push(l)}}catch(t){if(t instanceof e)throw t;b(t.message)}}));for(const[t,r]of[["protocolVersion","v"],["origin","o"],["sessionName","s"],["timing","t"]])if(void 0===p[t])throw new e(`missing mandatory "${r}=" line`,0,"");return p},C={parse:N,isValid:(e,t=!1)=>{try{return N(e),!0}catch(e){return t&&console.error(e),!1}}},A=e=>`c=${e.netType} ${e.addrType} ${e.connectionAddress}`,S=e=>`b=${e.bwtype}:${e.bandwidth}`,M=e=>`k=${e.method}${void 0!==e.encryptionKey?`:${e.encryptionKey}`:""}`,U=e=>Object.entries(e).map((([e,t])=>null===t?e:`${e}=${t}`)).join(";"),q=e=>{var t,r;return[`t=${e.startTime} ${e.stopTime}`,...(null!==(t=e.repeat)&&void 0!==t?t:[]).map((e=>`r=${[e.repeatInterval,e.activeDuration,...e.offsets].join(" ")}`)),...(null===(r=e.zoneAdjustments)||void 0===r?void 0:r.length)?[`z=${e.zoneAdjustments.map((e=>`${e.adjustmentTime} ${e.offset}`)).join(" ")}`]:[]]},K=e=>{var t;return[...void 0!==e.encodingName?[`a=rtpmap:${e.payloadType} ${e.encodingName}/${e.clockRate}${void 0!==e.encodingParameters?`/${e.encodingParameters}`:""}`]:[],...(null!==(t=e.rtcpFeedback)&&void 0!==t?t:[]).map((t=>`a=rtcp-fb:${e.payloadType} ${t}`)),...void 0!==e.parameters?[`a=fmtp:${e.payloadType} ${U(e.parameters)}`]:[]]},V=e=>{const t=[...e.payloadTypes?[`pt=${e.payloadTypes.join(",")}`]:[],...e.restrictions&&Object.keys(e.restrictions).length?[U(e.restrictions)]:[]].join(";");return`a=rid:${e.id} ${e.direction}${t?` ${t}`:""}`},D=(e,t)=>e?[`a=${t}`]:[],L=e=>{var t;return[...void 0!==e.iceUfrag?[`a=ice-ufrag:${e.iceUfrag}`]:[],...void 0!==e.icePwd?[`a=ice-pwd:${e.icePwd}`]:[],...e.iceOptions?[`a=ice-options:${e.iceOptions.join(" ")}`]:[],...(null!==(t=e.fingerprints)&&void 0!==t?t:[]).map((e=>`a=fingerprint:${e.hashFunction} ${e.fingerprint}`)),...void 0!==e.setup?[`a=setup:${e.setup}`]:[]]},W=e=>{var t;return[...(null!==(t=e.extmap)&&void 0!==t?t:[]).map((e=>`a=extmap:${e.id}${e.direction?`/${e.direction}`:""} ${e.uri}${void 0!==e.attributes?` ${e.attributes}`:""}`)),...D(e.extmapAllowMixed,"extmap-allow-mixed")]},G=e=>{var t;return(null!==(t=e.attributes)&&void 0!==t?t:[]).map((e=>void 0===e.value?`a=${e.name}`:`a=${e.name}:${e.value}`))},B=e=>{var t,r,i,s,n,o,a,d;const c=e.codecs?e.codecs.map((e=>String(e.payloadType))):e.formats;if(!c||0===c.length)throw new Error(`media section "${e.type}" has neither codecs nor formats`);return[`m=${e.type} ${e.port}${void 0!==e.numberOfPorts?`/${e.numberOfPorts}`:""} ${e.proto} ${c.join(" ")}`,...void 0!==e.information?[`i=${e.information}`]:[],...e.connectionInformation?[A(e.connectionInformation)]:[],...(null!==(t=e.bandwidths)&&void 0!==t?t:[]).map(S),...e.encryptionKey?[M(e.encryptionKey)]:[],...e.rtcp?[`a=rtcp:${e.rtcp.port}${void 0!==e.rtcp.netType?` ${e.rtcp.netType} ${e.rtcp.addrType} ${e.rtcp.address}`:""}`]:[],...L(e),...void 0!==e.mid?[`a=mid:${e.mid}`]:[],...W(e),...e.direction?[`a=${e.direction}`]:[],...(null!==(r=e.msid)&&void 0!==r?r:[]).map((e=>`a=msid:${e.streamId}${void 0!==e.trackId?` ${e.trackId}`:""}`)),...D(e.rtcpMux,"rtcp-mux"),...D(e.rtcpRsize,"rtcp-rsize"),...(null!==(i=e.codecs)&&void 0!==i?i:[]).flatMap(K),...(null!==(s=e.rtcpFeedbackWildcard)&&void 0!==s?s:[]).map((e=>`a=rtcp-fb:* ${e}`)),...void 0!==e.sctpPort?[`a=sctp-port:${e.sctpPort}`]:[],...void 0!==e.maxMessageSize?[`a=max-message-size:${e.maxMessageSize}`]:[],...(null!==(n=e.rids)&&void 0!==n?n:[]).map(V),...e.simulcast?[(p=e.simulcast,`a=simulcast:${["send","recv"].filter((e=>p[e])).map((e=>`${e} ${p[e].map((e=>e.map((e=>`${e.paused?"~":""}${e.rid}`)).join(","))).join(";")}`)).join(" ")}`)]:[],...(null!==(o=e.ssrcGroups)&&void 0!==o?o:[]).map((e=>`a=ssrc-group:${[e.semantics,...e.ssrcs].join(" ")}`)),...(null!==(a=e.ssrcs)&&void 0!==a?a:[]).flatMap((e=>Object.entries(e.attributes).map((([t,r])=>`a=ssrc:${e.id} ${t}${null===r?"":`:${r}`}`)))),...(null!==(d=e.candidates)&&void 0!==d?d:[]).map((e=>`a=candidate:${(e=>{var t;return[e.foundation,e.component,e.transport,e.priority,e.address,e.port,"typ",e.type,...void 0!==e.relatedAddress?["raddr",e.relatedAddress]:[],...void 0!==e.relatedPort?["rport",e.relatedPort]:[],...void 0!==e.tcpType?["tcptype",e.tcpType]:[],...Object.entries(null!==(t=e.extensions)&&void 0!==t?t:{}).flat()].join(" ")})(e)}`)),...D(e.endOfCandidates,"end-of-candidates"),...G(e)];var p},Z={write:e=>{var t,r,i,s,n,o,a,d;const{origin:c}=e;if(!c)throw new Error("origin (o=) is required");if(!(null===(t=e.timing)||void 0===t?void 0:t.length))throw new Error("at least one timing (t=) entry is required");return[`v=${null!==(r=e.protocolVersion)&&void 0!==r?r:0}`,`o=${c.username} ${c.sessId} ${c.sessVersion} ${c.netType} ${c.addrType} ${c.unicastAddress}`,`s=${null!==(i=e.sessionName)&&void 0!==i?i:"-"}`,...void 0!==e.sessionInformation?[`i=${e.sessionInformation}`]:[],...void 0!==e.uri?[`u=${e.uri}`]:[],...(null!==(s=e.emailAddresses)&&void 0!==s?s:[]).map((e=>`e=${e}`)),...(null!==(n=e.phoneNumbers)&&void 0!==n?n:[]).map((e=>`p=${e}`)),...e.connectionInformation?[A(e.connectionInformation)]:[],...(null!==(o=e.bandwidths)&&void 0!==o?o:[]).map(S),...e.timing.flatMap(q),...e.encryptionKey?[M(e.encryptionKey)]:[],...(null!==(a=e.group)&&void 0!==a?a:[]).map((e=>`a=group:${[e.semantics,...e.mids].join(" ")}`)),...D(e.iceLite,"ice-lite"),...L(e),...W(e),...e.direction?[`a=${e.direction}`]:[],...D(e.endOfCandidates,"end-of-candidates"),...G(e),...(null!==(d=e.media)&&void 0!==d?d:[]).flatMap(B)].join("\r\n")+"\r\n"}},_={validate:(t,r={})=>{var i,s,n,o;let d;if("string"==typeof t)try{d=C.parse(t)}catch(t){if(t instanceof e)return[{severity:"error",code:"parse-error",message:t.message,lineNumber:t.lineNumber}];throw t}else d=t;const c=[],p=(e,t,r)=>c.push(void 0===r?{severity:"error",code:e,message:t}:{severity:"error",code:e,message:t,mediaIndex:r}),l=(e,t,r)=>c.push(void 0===r?{severity:"warning",code:e,message:t}:{severity:"warning",code:e,message:t,mediaIndex:r}),m=null!==(i=d.media)&&void 0!==i?i:[];0!==d.protocolVersion&&p("invalid-version",`v= must be 0, got ${d.protocolVersion}`),d.origin||p("missing-origin","o= is required"),void 0===d.sessionName?p("missing-session-name","s= is required"):""===d.sessionName&&l("empty-session-name",'s= must not be empty; use "s= " or "s=-" (RFC 8866 §5.3)'),(null===(s=d.timing)||void 0===s?void 0:s.length)||p("missing-timing","at least one t= line is required"),d.connectionInformation||m.forEach(((e,t)=>{e.connectionInformation||p("missing-connection","c= is required at session level or in every media description (RFC 8866 §5.7)",t)}));const u=new Set;m.forEach(((e,t)=>{void 0!==e.mid&&(u.has(e.mid)&&p("duplicate-mid",`a=mid:${e.mid} is used by more than one media description`,t),u.add(e.mid))}));for(const e of null!==(n=d.group)&&void 0!==n?n:[])for(const t of e.mids)u.has(t)||p("group-unknown-mid",`a=group:${e.semantics} references unknown mid "${t}"`);(null===(o=d.group)||void 0===o?void 0:o.some((e=>"BUNDLE"===e.semantics)))&&m.forEach(((e,t)=>{void 0===e.mid&&p("bundle-missing-mid","every media description needs a=mid when BUNDLE is used (RFC 9143)",t)}));const f=void 0!==d.iceUfrag||void 0!==d.icePwd||d.iceLite||m.some((e=>{var t;return void 0!==e.iceUfrag||void 0!==e.icePwd||(null===(t=e.candidates)||void 0===t?void 0:t.length)})),v=(e,t,r)=>{void 0!==e&&(e.length<4||e.length>256)&&p("ice-ufrag-length",`ice-ufrag must be 4-256 characters, got ${e.length} (RFC 8839 §5.4)`,r),void 0!==t&&(t.length<22||t.length>256)&&p("ice-pwd-length",`ice-pwd must be 22-256 characters, got ${t.length} (RFC 8839 §5.4)`,r)};return v(d.iceUfrag,d.icePwd),m.forEach(((e,t)=>{var i,s,n,o,c,m,u,h,g,w,$,y,b,x,k,E;const T=0!==e.port;if(v(e.iceUfrag,e.icePwd,t),f&&T&&(void 0===(null!==(i=e.iceUfrag)&&void 0!==i?i:d.iceUfrag)&&p("missing-ice-ufrag","a=ice-ufrag is required (RFC 8839 §5.4)",t),void 0===(null!==(s=e.icePwd)&&void 0!==s?s:d.icePwd)&&p("missing-ice-pwd","a=ice-pwd is required (RFC 8839 §5.4)",t)),(e=>/(^|\/)(D?TLS)(\/|$)/.test(e.proto))(e)&&T){(null===(n=e.fingerprints)||void 0===n?void 0:n.length)||(null===(o=d.fingerprints)||void 0===o?void 0:o.length)||p("missing-fingerprint",`a=fingerprint is required for ${e.proto} (RFC 8842 §5.2, §5.3)`,t);const i=null!==(c=e.setup)&&void 0!==c?c:d.setup;void 0===i?p("missing-setup",`a=setup is required for ${e.proto} (RFC 8842 §5.2, §5.3)`,t):"answer"===r.type&&"actpass"===i&&p("setup-actpass-in-answer","an answer must use a=setup:active or a=setup:passive (RFC 5763 §5)",t)}if(a(e.proto)){for(const r of null!==(m=e.codecs)&&void 0!==m?m:[])r.payloadType>34&&void 0===r.encodingName&&p("missing-rtpmap",`payload type ${r.payloadType} is dynamic and needs a=rtpmap (RFC 8866 §6.6)`,t);for(const r of null!==(u=e.attributes)&&void 0!==u?u:[])"rtpmap"!==r.name&&"fmtp"!==r.name&&"rtcp-fb"!==r.name||l("unknown-payload-type",`a=${r.name}:${r.value} refers to a payload type that is not in the m= line, or repeats`,t)}const j=new Set;for(const r of null!==(h=e.extmap)&&void 0!==h?h:[])(r.id<1||15===r.id||r.id>255)&&p("extmap-invalid-id",`extmap id ${r.id} must be 1-14 or 16-255 (RFC 8285)`,t),j.has(r.id)&&p("extmap-duplicate-id",`extmap id ${r.id} is used more than once`,t),j.add(r.id);for(const r of["send","recv"])for(const i of(null!==(w=null===(g=e.simulcast)||void 0===g?void 0:g[r])&&void 0!==w?w:[]).flat())(null===($=e.rids)||void 0===$?void 0:$.some((e=>e.id===i.rid&&e.direction===r)))||p("simulcast-unknown-rid",`a=simulcast ${r} references "${i.rid}" without a matching a=rid:${i.rid} ${r} (RFC 8853 §5.1)`,t);for(const r of null!==(y=e.rids)&&void 0!==y?y:[])for(const i of null!==(b=r.payloadTypes)&&void 0!==b?b:[])(null===(x=e.codecs)||void 0===x?void 0:x.some((e=>e.payloadType===i)))||p("rid-unknown-payload-type",`a=rid:${r.id} references payload type ${i} that is not in the m= line (RFC 8851)`,t);for(const r of null!==(k=e.ssrcGroups)&&void 0!==k?k:[])for(const i of r.ssrcs)(null===(E=e.ssrcs)||void 0===E?void 0:E.some((e=>e.id===i)))||l("ssrc-group-unknown-ssrc",`a=ssrc-group:${r.semantics} references ssrc ${i} without an a=ssrc line`,t)})),c}},H="1.0.0";export{e as SdpParseError,C as sdpReader,_ as sdpValidator,Z as sdpWriter,H as version};
|
package/package.json
CHANGED
|
@@ -1,43 +1,67 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@irbisadm/simple-sdp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"license": "MIT",
|
|
5
|
-
"description": "
|
|
6
|
-
"author": {
|
|
7
|
-
"email": "i@irbisadm.dev",
|
|
8
|
-
"name": "Igor Sheko",
|
|
9
|
-
"url": "https://www.linkedin.com/in/irbisadm/"
|
|
10
|
-
},
|
|
11
|
-
"private": false,
|
|
12
|
-
"repository": "https://github.com/irbisadm/simple-sdp",
|
|
13
|
-
"bugs": "https://github.com/irbisadm/simple-sdp/issues",
|
|
14
|
-
"module": "./dist/index.mjs",
|
|
15
|
-
"main": "./dist/index.mjs",
|
|
16
|
-
"types": "./types/index.d.ts",
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"typescript"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@irbisadm/simple-sdp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Parse, modify, write and validate SDP (RFC 8866) for WebRTC and SIP. Typed ICE, DTLS, BUNDLE, simulcast, codecs. Zero dependencies.",
|
|
6
|
+
"author": {
|
|
7
|
+
"email": "i@irbisadm.dev",
|
|
8
|
+
"name": "Igor Sheko",
|
|
9
|
+
"url": "https://www.linkedin.com/in/irbisadm/"
|
|
10
|
+
},
|
|
11
|
+
"private": false,
|
|
12
|
+
"repository": "https://github.com/irbisadm/simple-sdp",
|
|
13
|
+
"bugs": "https://github.com/irbisadm/simple-sdp/issues",
|
|
14
|
+
"module": "./dist/index.mjs",
|
|
15
|
+
"main": "./dist/index.mjs",
|
|
16
|
+
"types": "./types/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./types/index.d.ts",
|
|
20
|
+
"import": "./dist/index.mjs",
|
|
21
|
+
"default": "./dist/index.mjs"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"keywords": [
|
|
26
|
+
"sdp",
|
|
27
|
+
"webrtc",
|
|
28
|
+
"sip",
|
|
29
|
+
"rfc8866",
|
|
30
|
+
"rfc4566",
|
|
31
|
+
"sdp-writer",
|
|
32
|
+
"sdp-validator",
|
|
33
|
+
"sdp-parser",
|
|
34
|
+
"typescript",
|
|
35
|
+
"parser",
|
|
36
|
+
"simulcast",
|
|
37
|
+
"ice"
|
|
38
|
+
],
|
|
39
|
+
"files": [
|
|
40
|
+
"./dist/index.mjs",
|
|
41
|
+
"./types/index.d.ts",
|
|
42
|
+
"./readme.md",
|
|
43
|
+
"./CHANGELOG.md"
|
|
44
|
+
],
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@rollup/plugin-replace": "^5.0.2",
|
|
47
|
+
"@rollup/plugin-terser": "^0.4.0",
|
|
48
|
+
"@rollup/plugin-typescript": "^11.0.0",
|
|
49
|
+
"@types/jest": "^29.5.0",
|
|
50
|
+
"@types/node": "^18.14.0",
|
|
51
|
+
"jest": "^29.5.0",
|
|
52
|
+
"rollup": "^3.17.1",
|
|
53
|
+
"rollup-plugin-dts": "^5.2.0",
|
|
54
|
+
"ts-jest": "^29.0.5",
|
|
55
|
+
"tslib": "^2.5.0",
|
|
56
|
+
"typescript": "^5.0.2"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=17.0.0"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "rollup -c",
|
|
63
|
+
"test": "jest",
|
|
64
|
+
"typecheck": "tsc --noEmit"
|
|
65
|
+
},
|
|
66
|
+
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
67
|
+
}
|
package/readme.md
CHANGED
|
@@ -1,23 +1,188 @@
|
|
|
1
|
-

|
|
2
|
-

|
|
3
|
-

|
|
4
|
-

|
|
5
|
-

|
|
6
|
-

|
|
2
|
+

|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
# simple-sdp
|
|
9
|
+
|
|
10
|
+
Parse, modify, write and validate Session Description Protocol
|
|
11
|
+
([RFC 8866](https://www.rfc-editor.org/rfc/rfc8866)) messages for WebRTC and SIP.
|
|
12
|
+
TypeScript, no runtime dependencies, works in Node.js and browsers.
|
|
13
|
+
|
|
14
|
+
- **`sdpReader.parse`**: strict. Line order and mandatory lines are checked against
|
|
15
|
+
RFC 8866 §5. Every error is an `SdpParseError` with a line number.
|
|
16
|
+
- **Lossless model**: each line maps to a typed field or, for attributes the library
|
|
17
|
+
does not interpret, to an ordered `attributes` entry with the exact original value.
|
|
18
|
+
- **`sdpWriter.write`**: serializes the model back to RFC 8866 text (CRLF).
|
|
19
|
+
`write(parse(x))` keeps every line of `x` (see [Writing](#writing)).
|
|
20
|
+
- **`sdpValidator.validate`**: reports RFC and WebRTC rule violations as a list of issues.
|
|
21
|
+
- **Typed WebRTC attributes**: ICE, DTLS, BUNDLE, codecs, simulcast, SSRCs (see below).
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install @irbisadm/simple-sdp
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Parse → modify → write
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import {sdpReader, sdpWriter, sdpValidator} from '@irbisadm/simple-sdp';
|
|
33
|
+
|
|
34
|
+
const sdp = sdpReader.parse(offer.sdp);
|
|
35
|
+
|
|
36
|
+
// Keep only Opus and ask for stereo
|
|
37
|
+
for (const m of sdp.media.filter(m => m.type === 'audio')) {
|
|
38
|
+
m.codecs = m.codecs?.filter(c => c.encodingName === 'opus');
|
|
39
|
+
m.codecs?.forEach(c => c.parameters = {...c.parameters, stereo: '1'});
|
|
40
|
+
}
|
|
41
|
+
sdp.origin.sessVersion = String(BigInt(sdp.origin.sessVersion) + 1n);
|
|
42
|
+
|
|
43
|
+
sdpValidator.validate(sdp, {type: 'offer'}); // [] — no issues
|
|
44
|
+
await pc.setLocalDescription({type: 'offer', sdp: sdpWriter.write(sdp)});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The `m=` payload type list is derived from `codecs`, so removing a codec also removes
|
|
48
|
+
its `rtpmap`, `fmtp` and `rtcp-fb` lines.
|
|
49
|
+
|
|
50
|
+
### Read
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import {sdpReader, SdpParseError} from '@irbisadm/simple-sdp';
|
|
54
|
+
|
|
55
|
+
const sdp = sdpReader.parse(pc.localDescription.sdp);
|
|
56
|
+
|
|
57
|
+
sdp.group; // [{semantics: 'BUNDLE', mids: ['0', '1']}]
|
|
58
|
+
const [audio] = sdp.media;
|
|
59
|
+
audio.mid; // '0'
|
|
60
|
+
audio.direction; // 'sendrecv'
|
|
61
|
+
audio.codecs?.[0]; // {payloadType: 111, encodingName: 'opus', clockRate: 48000,
|
|
62
|
+
// encodingParameters: 2, parameters: {minptime: '10', useinbandfec: '1'},
|
|
63
|
+
// rtcpFeedback: ['transport-cc']}
|
|
64
|
+
audio.candidates?.[0].type; // 'host'
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
sdpReader.parse('v=0\r\ns=-\r\no=- 1 1 IN IP4 127.0.0.1\r\nt=0 0\r\n');
|
|
68
|
+
} catch (e) {
|
|
69
|
+
if (e instanceof SdpParseError) {
|
|
70
|
+
e.lineNumber; // 3
|
|
71
|
+
e.reason; // 'line is out of order (session order: v o s i u e p c b t r z k a)'
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`sdpReader.isValid(text)` returns `true` if `parse` accepts the text.
|
|
77
|
+
|
|
78
|
+
## Supported lines and attributes
|
|
79
|
+
|
|
80
|
+
All line types of RFC 8866 §5 are supported: `v o s i u e p c b t r z k a m`, at session
|
|
81
|
+
and media level. `r=`/`z=` time units (`d h m s`) are converted to seconds.
|
|
82
|
+
|
|
83
|
+
| Attribute | Field | Spec |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `sendrecv` `sendonly` `recvonly` `inactive` | `direction` | RFC 8866 §6.7 |
|
|
86
|
+
| `group` | `group[]` | RFC 5888, RFC 9143 |
|
|
87
|
+
| `mid` | `mid` | RFC 5888 |
|
|
88
|
+
| `rtpmap` `fmtp` `rtcp-fb` | `codecs[]` (per payload type), `rtcpFeedbackWildcard` | RFC 8866, RFC 4585 |
|
|
89
|
+
| `extmap` `extmap-allow-mixed` | `extmap[]`, `extmapAllowMixed` | RFC 8285 |
|
|
90
|
+
| `ice-ufrag` `ice-pwd` `ice-options` `ice-lite` | `iceUfrag` `icePwd` `iceOptions` `iceLite` | RFC 8839 |
|
|
91
|
+
| `candidate` | `candidates[]` | RFC 8839, RFC 6544 (`tcptype`) |
|
|
92
|
+
| `end-of-candidates` | `endOfCandidates` | RFC 8840 |
|
|
93
|
+
| `fingerprint` | `fingerprints[]` | RFC 8122 |
|
|
94
|
+
| `setup` | `setup` | RFC 4145 |
|
|
95
|
+
| `msid` | `msid[]` | RFC 8830 |
|
|
96
|
+
| `ssrc` `ssrc-group` | `ssrcs[]` `ssrcGroups[]` | RFC 5576 |
|
|
97
|
+
| `rid` | `rids[]` | RFC 8851 |
|
|
98
|
+
| `simulcast` | `simulcast` | RFC 8853 |
|
|
99
|
+
| `rtcp` | `rtcp` | RFC 3605 |
|
|
100
|
+
| `rtcp-mux` `rtcp-rsize` | `rtcpMux` `rtcpRsize` | RFC 5761, RFC 5506 |
|
|
101
|
+
| `sctp-port` `max-message-size` | `sctpPort` `maxMessageSize` | RFC 8841 |
|
|
102
|
+
|
|
103
|
+
Other attributes go to `attributes: {name, value?}[]` in their original order.
|
|
104
|
+
So do known attributes outside their scope (e.g. `a=mid` at session level) and
|
|
105
|
+
`rtpmap`/`fmtp`/`rtcp-fb` for a payload type that is not in the `m=` line.
|
|
106
|
+
|
|
107
|
+
## Writing
|
|
108
|
+
|
|
109
|
+
`sdpWriter.write(sdp)` emits lines in RFC 8866 §5 order, each terminated by CRLF.
|
|
110
|
+
It throws if `origin` or `timing` is missing, or if a media section has neither `codecs`
|
|
111
|
+
nor `formats`.
|
|
112
|
+
|
|
113
|
+
The output carries the same lines as the input, but the order of attributes inside a
|
|
114
|
+
section is normalized. Session level: `group`, `ice-lite`, ICE, DTLS, `extmap`, direction,
|
|
115
|
+
`end-of-candidates`, then generic attributes. Media level: `rtcp`, ICE, DTLS, `mid`,
|
|
116
|
+
`extmap`, direction, `msid`, `rtcp-mux`, `rtcp-rsize`, per codec `rtpmap` → `rtcp-fb` → `fmtp`,
|
|
117
|
+
`sctp-port`, `max-message-size`, `rid`, `simulcast`, `ssrc-group`, `ssrc`, `candidate`,
|
|
118
|
+
`end-of-candidates`, then generic attributes in their original order.
|
|
119
|
+
Other normalizations:
|
|
120
|
+
|
|
121
|
+
- `r=`/`z=` times are written in seconds (`7d` → `604800`);
|
|
122
|
+
- whitespace around `;` in `a=fmtp` is dropped;
|
|
123
|
+
- `a=ssrc` lines are grouped by SSRC; `a=simulcast` lists `send` before `recv`.
|
|
124
|
+
|
|
125
|
+
The round-trip test checks every fixture: the lines of each section are the same as in
|
|
126
|
+
the input, non-attribute lines keep their order, and `parse(write(parse(x)))` deep-equals
|
|
127
|
+
`parse(x)`.
|
|
128
|
+
|
|
129
|
+
## Validation
|
|
130
|
+
|
|
131
|
+
`sdpValidator.validate(sdp | text, {type?: 'offer' | 'answer'})` returns
|
|
132
|
+
`SdpIssue[]` (`{severity, code, message, mediaIndex?, lineNumber?}`). Text is parsed first.
|
|
133
|
+
A parse failure is reported as one `parse-error` issue, so the RFC 8866 §5 order rules
|
|
134
|
+
are covered as well.
|
|
135
|
+
|
|
136
|
+
| Code | Severity | Rule |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| `parse-error` | error | `sdpReader.parse` failed |
|
|
139
|
+
| `invalid-version`, `missing-origin`, `missing-session-name`, `missing-timing` | error | mandatory `v=`/`o=`/`s=`/`t=` (for hand-built objects) |
|
|
140
|
+
| `empty-session-name` | warning | `s=` must not be empty (RFC 8866 §5.3) |
|
|
141
|
+
| `missing-connection` | error | `c=` at session level or in every media description (RFC 8866 §5.7) |
|
|
142
|
+
| `duplicate-mid`, `group-unknown-mid`, `bundle-missing-mid` | error | unique `mid`s; group members exist; every media has `mid` with BUNDLE (RFC 5888, RFC 9143) |
|
|
143
|
+
| `missing-rtpmap` | error | payload types above 34 need `a=rtpmap` (RFC 8866 §6.6, RFC 3551) |
|
|
144
|
+
| `unknown-payload-type` | warning | `rtpmap`/`fmtp`/`rtcp-fb` for a payload type not in `m=` |
|
|
145
|
+
| `missing-fingerprint`, `missing-setup` | error | DTLS transports (`UDP/TLS/RTP/SAVPF`, `UDP/DTLS/SCTP`, …) (RFC 8842 §5.2, §5.3) |
|
|
146
|
+
| `setup-actpass-in-answer` | error | with `type: 'answer'` (RFC 5763 §5) |
|
|
147
|
+
| `missing-ice-ufrag`, `missing-ice-pwd` | error | if ICE is used, every active media has credentials (RFC 8839 §5.4) |
|
|
148
|
+
| `ice-ufrag-length`, `ice-pwd-length` | error | 4–256 / 22–256 characters (RFC 8839 §5.4) |
|
|
149
|
+
| `extmap-invalid-id`, `extmap-duplicate-id` | error | ids 1–14 or 16–255, unique per section (RFC 8285) |
|
|
150
|
+
| `simulcast-unknown-rid`, `rid-unknown-payload-type` | error | `a=simulcast` rids exist with the same direction; `pt=` values are in `m=` (RFC 8851, RFC 8853) |
|
|
151
|
+
| `ssrc-group-unknown-ssrc` | warning | `a=ssrc-group` members have `a=ssrc` lines (RFC 5576) |
|
|
152
|
+
|
|
153
|
+
Media sections with port 0 (rejected or bundle-only) are skipped by the ICE and DTLS checks.
|
|
154
|
+
|
|
155
|
+
## Parsing rules
|
|
156
|
+
|
|
157
|
+
`sdpReader.parse` throws `SdpParseError` when:
|
|
158
|
+
|
|
159
|
+
- a line is not `<type>=<value>`, is empty, or has an unknown type;
|
|
160
|
+
- lines are out of the RFC 8866 §5 order (session `v o s i u e p c b t r z k a`, media `m i c b k a`)
|
|
161
|
+
or a single-occurrence line repeats;
|
|
162
|
+
- `v=`, `o=`, `s=` or `t=` is missing, or `v` is not `0`;
|
|
163
|
+
- a typed line or attribute is malformed (e.g. `a=candidate` without `typ`, `a=setup:client`);
|
|
164
|
+
- a single-occurrence attribute repeats (`a=mid`, `a=ice-ufrag`, two directions, …).
|
|
165
|
+
|
|
166
|
+
Accepted deviations: LF-only line endings (RFC 8866 §5 says parsers SHOULD accept them),
|
|
167
|
+
a missing final line terminator, RFC 4566-style `z=` after several `t=` lines.
|
|
168
|
+
|
|
169
|
+
Known limitation: more than one `c=` line in a media description (layered multicast)
|
|
170
|
+
is rejected.
|
|
171
|
+
|
|
172
|
+
## Compatibility
|
|
173
|
+
|
|
174
|
+
Tested on real session descriptions (see [`test/fixtures`](test/fixtures/README.md)).
|
|
175
|
+
Every fixture parses, round-trips through the writer and validates without issues:
|
|
176
|
+
|
|
177
|
+
| Source | Fixtures |
|
|
178
|
+
|---|---|
|
|
179
|
+
| Chrome 156 | audio+video, audio-only, datachannel, simulcast (`rid` + `simulcast`): offer and answer |
|
|
180
|
+
| Firefox 159 | audio+video, audio-only, datachannel, simulcast: offer and answer |
|
|
181
|
+
| Chrome (2023) | audio+video, datachannel, a perfect-negotiation sequence of 7 descriptions |
|
|
182
|
+
| SIP, RFC 4566 style, no ICE/DTLS | Polycom SoundPoint IP 330 offer, FreeSWITCH 1.0.4 answer, LG IP phone answer, carrier gateway offer with static payload types and no `rtpmap` |
|
|
183
|
+
|
|
184
|
+
Safari is not covered by fixtures yet.
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT
|
package/types/index.d.ts
CHANGED
|
@@ -1,303 +1,337 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Session description model.
|
|
3
|
+
*
|
|
4
|
+
* The model is lossless: every line of a parsed SDP is represented either by a typed
|
|
5
|
+
* field or, for attributes the library does not interpret, by an entry in `attributes`.
|
|
6
|
+
*/
|
|
7
|
+
/** RFC 8866 §6.7 */
|
|
8
|
+
type SdpDirection = 'sendrecv' | 'sendonly' | 'recvonly' | 'inactive';
|
|
9
|
+
/** RFC 4145 §4, used by DTLS-SRTP (RFC 5763) */
|
|
10
|
+
type SdpSetup = 'active' | 'passive' | 'actpass' | 'holdconn';
|
|
11
|
+
/**
|
|
12
|
+
* An attribute the library does not interpret, kept as-is.
|
|
13
|
+
* `a=<name>` is `{name}`; `a=<name>:<value>` is `{name, value}`. The value is kept
|
|
14
|
+
* verbatim, including leading whitespace (e.g. `a=msid-semantic: WMS`).
|
|
15
|
+
*/
|
|
16
|
+
interface SdpGenericAttribute {
|
|
17
|
+
name: string;
|
|
18
|
+
value?: string;
|
|
5
19
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
encryptionKeys?: SdpEncryptionKey[];
|
|
42
|
-
/**
|
|
43
|
-
* The "t=" lines specify the start and stop times for a session. Multiple "t=" lines MAY be used if a session is
|
|
44
|
-
* active at multiple irregularly spaced times; each additional "t=" line specifies an additional period of time for
|
|
45
|
-
* which the session will be active. If the session is active at regular times, an "r=" line (see below) should be
|
|
46
|
-
* used in addition to, and following, a "t=" line -- in which case the "t=" line specifies the start and stop times
|
|
47
|
-
* of the repeat sequence.
|
|
48
|
-
*/
|
|
49
|
-
timing?: SdpTiming[];
|
|
50
|
-
/**
|
|
51
|
-
* "r=" fields specify repeat times for a session. For example, if a session is active at 10am on Monday and 11am on
|
|
52
|
-
* Tuesday for one hour each week for three months, then the <start-time> in the corresponding "t=" field would be the
|
|
53
|
-
* NTP representation of 10am on the first Monday, the <repeat interval> would be 1 week, the <active duration> would
|
|
54
|
-
* be 1 hour, and the offsets would be zero and 25 hours. The corresponding "t=" field stop time would be the NTP
|
|
55
|
-
* representation of the end of the last session three months later.
|
|
56
|
-
*/
|
|
57
|
-
repeat?: SdpRepeatTimes[];
|
|
58
|
-
media?: SdpMediaSection[];
|
|
59
|
-
bandwidthInformation?: SdpBandwidthInformation;
|
|
60
|
-
connectionInformation?: SdpConnectionInformation;
|
|
61
|
-
group?: SdpGroup[];
|
|
20
|
+
/** `a=fingerprint` — RFC 8122 §5 */
|
|
21
|
+
interface SdpFingerprint {
|
|
22
|
+
/** e.g. `sha-256` */
|
|
23
|
+
hashFunction: string;
|
|
24
|
+
/** Upper-case hex pairs separated by colons */
|
|
25
|
+
fingerprint: string;
|
|
26
|
+
}
|
|
27
|
+
/** `a=extmap` — RFC 8285 §8 */
|
|
28
|
+
interface SdpExtmap {
|
|
29
|
+
id: number;
|
|
30
|
+
direction?: SdpDirection;
|
|
31
|
+
uri: string;
|
|
32
|
+
/** Extension attributes, verbatim */
|
|
33
|
+
attributes?: string;
|
|
34
|
+
}
|
|
35
|
+
/** `a=candidate` — RFC 8839 §5.1 (TCP candidates: RFC 6544) */
|
|
36
|
+
interface SdpCandidate {
|
|
37
|
+
foundation: string;
|
|
38
|
+
/** 1 = RTP, 2 = RTCP */
|
|
39
|
+
component: number;
|
|
40
|
+
/** `UDP`, `TCP`, as written (case is not normalized) */
|
|
41
|
+
transport: string;
|
|
42
|
+
priority: number;
|
|
43
|
+
/** IP address or FQDN (e.g. an mDNS `*.local` name) */
|
|
44
|
+
address: string;
|
|
45
|
+
port: number;
|
|
46
|
+
type: 'host' | 'srflx' | 'prflx' | 'relay' | (string & {});
|
|
47
|
+
/** `raddr` */
|
|
48
|
+
relatedAddress?: string;
|
|
49
|
+
/** `rport` */
|
|
50
|
+
relatedPort?: number;
|
|
51
|
+
/** `tcptype` — RFC 6544 */
|
|
52
|
+
tcpType?: 'active' | 'passive' | 'so' | (string & {});
|
|
53
|
+
/** Other extension attributes in their original order, e.g. `generation`, `ufrag`, `network-id` */
|
|
54
|
+
extensions?: Record<string, string>;
|
|
62
55
|
}
|
|
63
56
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
57
|
+
* All `a=ssrc:<ssrc-id> <attribute>[:<value>]` lines of one SSRC — RFC 5576 §4.1.
|
|
58
|
+
* Lines of the same SSRC are merged, in order of first appearance.
|
|
66
59
|
*/
|
|
67
|
-
interface
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* <addrtype> is a text string giving the type of the address that follows. Initially "IP4" and "IP6" are defined,
|
|
93
|
-
* but other values MAY be registered in the future
|
|
94
|
-
*/
|
|
95
|
-
addrType?: 'IP4' | 'IP6';
|
|
60
|
+
interface SdpSsrc {
|
|
61
|
+
id: number;
|
|
62
|
+
/** Source attributes in their original order, e.g. `cname`, `msid`. Attributes without a value map to null. */
|
|
63
|
+
attributes: Record<string, string | null>;
|
|
64
|
+
}
|
|
65
|
+
/** `a=ssrc-group:<semantics> <ssrc-id> ...` — RFC 5576 §4.2 */
|
|
66
|
+
interface SdpSsrcGroup {
|
|
67
|
+
/** `FID`, `FEC`, `FEC-FR`, `SIM`, … */
|
|
68
|
+
semantics: string;
|
|
69
|
+
ssrcs: number[];
|
|
70
|
+
}
|
|
71
|
+
/** `a=msid:<stream id> [<track id>]` — RFC 8830 §2 */
|
|
72
|
+
interface SdpMsid {
|
|
73
|
+
/** msid-id; `-` means "no stream" */
|
|
74
|
+
streamId: string;
|
|
75
|
+
/** msid-appdata; WebRTC uses it for the track id */
|
|
76
|
+
trackId?: string;
|
|
77
|
+
}
|
|
78
|
+
/** `a=rid:<rid-id> <send|recv> [pt=<fmt>,...;][<restriction>;...]` — RFC 8851 §10 */
|
|
79
|
+
interface SdpRid {
|
|
80
|
+
id: string;
|
|
81
|
+
direction: 'send' | 'recv';
|
|
82
|
+
/** `pt=` list */
|
|
83
|
+
payloadTypes?: number[];
|
|
96
84
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* IP version 4 address of the machine. For an address type of IP6, this is either the fully qualified domain name of
|
|
100
|
-
* the machine or the compressed textual representation of the IP version 6 address of the machine. For both IP4
|
|
101
|
-
* and IP6, the fully qualified domain name is the form that SHOULD be given unless this is unavailable, in which case
|
|
102
|
-
* the globally unique address MAY be substituted. A local IP address MUST NOT be used in any context where the SDP
|
|
103
|
-
* description might leave the scope in which the address is meaningful (for example, a local address MUST NOT be
|
|
104
|
-
* included in an application-level referral that might leave the scope).
|
|
85
|
+
* Restrictions in their original order, e.g. `max-width`, `max-fps`, `depend`.
|
|
86
|
+
* A restriction without `=` maps to null.
|
|
105
87
|
*/
|
|
106
|
-
|
|
88
|
+
restrictions?: Record<string, string | null>;
|
|
89
|
+
}
|
|
90
|
+
/** One rid of an `a=simulcast` stream — RFC 8853 §5.1 */
|
|
91
|
+
interface SdpSimulcastRid {
|
|
92
|
+
rid: string;
|
|
93
|
+
/** Prefixed with `~`: initially paused (RFC 7728) */
|
|
94
|
+
paused?: boolean;
|
|
107
95
|
}
|
|
108
96
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* which the session will be active. If the session is active at regular times, an "r=" line (see below) should be
|
|
112
|
-
* used in addition to, and following, a "t=" line -- in which case the "t=" line specifies the start and stop times
|
|
113
|
-
* of the repeat sequence.
|
|
97
|
+
* Simulcast streams of one direction. Every element is one simulcast stream (`;`-separated),
|
|
98
|
+
* listing its alternative rids (`,`-separated).
|
|
114
99
|
*/
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
100
|
+
type SdpSimulcastStreams = SdpSimulcastRid[][];
|
|
101
|
+
/** `a=simulcast` — RFC 8853 §5.1 */
|
|
102
|
+
interface SdpSimulcast {
|
|
103
|
+
send?: SdpSimulcastStreams;
|
|
104
|
+
recv?: SdpSimulcastStreams;
|
|
105
|
+
}
|
|
106
|
+
/** `a=rtcp:<port> [<nettype> <addrtype> <connection-address>]` — RFC 3605 */
|
|
107
|
+
interface SdpRtcp {
|
|
108
|
+
port: number;
|
|
109
|
+
netType?: string;
|
|
110
|
+
addrType?: string;
|
|
111
|
+
address?: string;
|
|
112
|
+
}
|
|
113
|
+
/** Attributes allowed both at session level and at media level */
|
|
114
|
+
interface SdpCommonAttributes {
|
|
115
|
+
/** RFC 8866 §6.7. At session level it is the default for all media sections. */
|
|
116
|
+
direction?: SdpDirection;
|
|
117
|
+
/** `a=ice-ufrag` — RFC 8839 §5.4 */
|
|
118
|
+
iceUfrag?: string;
|
|
119
|
+
/** `a=ice-pwd` — RFC 8839 §5.4 */
|
|
120
|
+
icePwd?: string;
|
|
121
|
+
/** `a=ice-options` — RFC 8839 §5.6 */
|
|
122
|
+
iceOptions?: string[];
|
|
123
|
+
/** `a=end-of-candidates` — RFC 8840 §8.2 */
|
|
124
|
+
endOfCandidates?: boolean;
|
|
125
|
+
/** `a=fingerprint` — RFC 8122 §5. Several lines are allowed. */
|
|
126
|
+
fingerprints?: SdpFingerprint[];
|
|
127
|
+
/** `a=setup` — RFC 4145 §4 */
|
|
128
|
+
setup?: SdpSetup;
|
|
129
|
+
/** `a=extmap` — RFC 8285 */
|
|
130
|
+
extmap?: SdpExtmap[];
|
|
131
|
+
/** `a=extmap-allow-mixed` — RFC 8285 §6 */
|
|
132
|
+
extmapAllowMixed?: boolean;
|
|
133
|
+
/** Attributes without a typed representation, in their original order */
|
|
134
|
+
attributes?: SdpGenericAttribute[];
|
|
118
135
|
}
|
|
119
136
|
/**
|
|
120
|
-
*
|
|
121
|
-
* Tuesday for one hour each week for three months, then the <start-time> in the corresponding "t=" field would be the
|
|
122
|
-
* NTP representation of 10am on the first Monday, the <repeat interval> would be 1 week, the <active duration> would
|
|
123
|
-
* be 1 hour, and the offsets would be zero and 25 hours. The corresponding "t=" field stop time would be the NTP
|
|
124
|
-
* representation of the end of the last session three months later.
|
|
137
|
+
* `o=<username> <sess-id> <sess-version> <nettype> <addrtype> <unicast-address>` — RFC 8866 §5.2
|
|
125
138
|
*/
|
|
139
|
+
interface SdpOrigin {
|
|
140
|
+
/** User's login on the originating host, or `-` */
|
|
141
|
+
username: string;
|
|
142
|
+
/**
|
|
143
|
+
* Numeric string. Kept as a string because implementations use 64-bit values
|
|
144
|
+
* that do not fit into a JavaScript number.
|
|
145
|
+
*/
|
|
146
|
+
sessId: string;
|
|
147
|
+
/** Numeric string, see {@link sessId}. Increase it on every modification. */
|
|
148
|
+
sessVersion: string;
|
|
149
|
+
/** `IN` */
|
|
150
|
+
netType: string;
|
|
151
|
+
/** `IP4` | `IP6` */
|
|
152
|
+
addrType: string;
|
|
153
|
+
unicastAddress: string;
|
|
154
|
+
}
|
|
155
|
+
/** `r=<repeat interval> <active duration> <offsets from start-time>` — RFC 8866 §5.10. All values in seconds. */
|
|
126
156
|
interface SdpRepeatTimes {
|
|
127
157
|
repeatInterval: number;
|
|
128
158
|
activeDuration: number;
|
|
129
|
-
offsets
|
|
159
|
+
offsets: number[];
|
|
160
|
+
}
|
|
161
|
+
/** One `<adjustment time> <offset>` pair of `z=` — RFC 8866 §5.11. Values in seconds. */
|
|
162
|
+
interface SdpTimeZoneAdjustment {
|
|
163
|
+
adjustmentTime: number;
|
|
164
|
+
offset: number;
|
|
130
165
|
}
|
|
131
166
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* supported for compatibility with older implementations and its use is NOT RECOMMENDED. Work is in progress to define
|
|
135
|
-
* new key exchange mechanisms for use with SDP [27] [28], and it is expected that new applications will use
|
|
136
|
-
* those mechanisms.
|
|
137
|
-
*
|
|
138
|
-
* A key field is permitted before the first media entry (in which case it applies to all media in the session), or
|
|
139
|
-
* for each media entry as required. The format of keys and their usage are outside the scope of this document, and
|
|
140
|
-
* the key field provides no way to indicate the encryption algorithm to be used, key type, or other information about
|
|
141
|
-
* the key: this is assumed to be provided by the higher-level protocol using SDP. If there is a need to convey this
|
|
142
|
-
* information within SDP, the extensions mentioned previously SHOULD be used. Many security protocols require two
|
|
143
|
-
* keys: one for confidentiality, another for integrity. This specification does not support transfer of two keys.
|
|
167
|
+
* Time description: `t=` with its `r=` and `z=` lines — RFC 8866 §5.9–5.11.
|
|
168
|
+
* Times are NTP seconds; `0` means unbounded.
|
|
144
169
|
*/
|
|
170
|
+
interface SdpTiming {
|
|
171
|
+
startTime: number;
|
|
172
|
+
stopTime: number;
|
|
173
|
+
repeat?: SdpRepeatTimes[];
|
|
174
|
+
zoneAdjustments?: SdpTimeZoneAdjustment[];
|
|
175
|
+
}
|
|
176
|
+
/** `k=<method>[:<encryption key>]` — RFC 8866 §5.12 (obsolete) */
|
|
145
177
|
interface SdpEncryptionKey {
|
|
146
|
-
|
|
147
|
-
* The method indicates the mechanism to be used to obtain a usable key by external means, or from the encoded
|
|
148
|
-
* encryption key given. The following methods are defined:
|
|
149
|
-
*
|
|
150
|
-
* clear
|
|
151
|
-
* The encryption key is included untransformed in this key field. This method MUST NOT be used unless it can be
|
|
152
|
-
* guaranteed that the SDP is conveyed over a secure channel. The encryption key is interpreted as text according
|
|
153
|
-
* to the charset attribute; use the "k=base64:" method to convey characters that are otherwise prohibited in SDP.
|
|
154
|
-
*
|
|
155
|
-
* base64
|
|
156
|
-
* The encryption key is included in this key field but has been base64 encoded because it includes characters
|
|
157
|
-
* that are prohibited in SDP. This method MUST NOT be used unless it can be guaranteed that the SDP is
|
|
158
|
-
* conveyed over a secure channel.
|
|
159
|
-
*
|
|
160
|
-
* uri
|
|
161
|
-
* A Uniform Resource Identifier is included in the key field. The URI refers to the data containing the key,
|
|
162
|
-
* and may require additional authentication before the key can be returned. When a request is made to the given
|
|
163
|
-
* URI, the reply should specify the encoding for the key. The URI is often an Secure Socket Layer/Transport
|
|
164
|
-
* Layer Security (SSL/TLS)-protected HTTP URI ("https:"), although this is not required.
|
|
165
|
-
*
|
|
166
|
-
* prompt
|
|
167
|
-
* No key is included in this SDP description, but the session or media stream referred to by this key field
|
|
168
|
-
* is encrypted. The user should be prompted for the key when attempting to join the session, and this
|
|
169
|
-
* user-supplied key should then be used to decrypt the media streams. The use of user-specified keys is
|
|
170
|
-
* NOT RECOMMENDED, since such keys tend to have weak security properties.
|
|
171
|
-
*/
|
|
172
|
-
method: 'clear' | 'base64' | 'uri' | 'prompt';
|
|
178
|
+
method: 'clear' | 'base64' | 'uri' | 'prompt' | (string & {});
|
|
173
179
|
encryptionKey?: string;
|
|
174
180
|
}
|
|
175
|
-
/**
|
|
176
|
-
* The OPTIONAL "b=" line (bandwidth-field) denotes the proposed bandwidth to be used by the session or media
|
|
177
|
-
* description. The <bwtype> is an alphanumeric modifier that provides the meaning of the <bandwidth> number. Two
|
|
178
|
-
* values are defined in this specification, but other values MAY be registered in the future
|
|
179
|
-
* (see Section 8 and [RFC3556], [RFC3890]):
|
|
180
|
-
*/
|
|
181
|
+
/** `b=<bwtype>:<bandwidth>` — RFC 8866 §5.8, RFC 3890 (TIAS) */
|
|
181
182
|
interface SdpBandwidthInformation {
|
|
182
|
-
/**
|
|
183
|
-
* CT
|
|
184
|
-
* If the bandwidth of a session is different from the bandwidth implicit from the scope, a "b=CT:" line SHOULD
|
|
185
|
-
* be supplied for the session giving the proposed upper limit to the bandwidth used
|
|
186
|
-
* (the "conference total" bandwidth). Similarly, if the bandwidth of bundled media streams [RFC8843] in
|
|
187
|
-
* an "m=" line is different from the implicit value from the scope, a "b=CT:" line SHOULD be supplied in the
|
|
188
|
-
* media level. The primary purpose of this is to give an approximate idea as to whether two or more sessions
|
|
189
|
-
* (or bundled media streams) can coexist simultaneously. Note that a "b=CT:" line gives a total bandwidth figure
|
|
190
|
-
* for all the media at all endpoints.
|
|
191
|
-
*
|
|
192
|
-
* The Mux Category for "b=CT:" is NORMAL. This is discussed in [RFC8859].
|
|
193
|
-
*
|
|
194
|
-
* AS
|
|
195
|
-
* The bandwidth is interpreted to be application specific (it will be the application's concept
|
|
196
|
-
* of maximum bandwidth). Normally, this will coincide with what is set on the application's "maximum bandwidth"
|
|
197
|
-
* control if applicable. For RTP-based applications, the "b=AS:" line gives the RTP "session bandwidth" as defined
|
|
198
|
-
* in Section 6.2 of [RFC3550]. Note that a "b=AS:" line gives a bandwidth figure for a single media at
|
|
199
|
-
* a single endpoint, although there may be many endpoints sending simultaneously.
|
|
200
|
-
*
|
|
201
|
-
* The Mux Category for "b=AS:" is SUM. This is discussed in [RFC8859].
|
|
202
|
-
*
|
|
203
|
-
* [RFC4566] defined an "X-" prefix for <bwtype> names. This was intended for experimental purposes only.
|
|
204
|
-
* Use of the "X-" prefix is NOT RECOMMENDED. Instead new (non "X-" prefix) <bwtype> names SHOULD be defined,
|
|
205
|
-
* and then MUST be registered with IANA in the standard namespace. SDP parsers MUST ignore bandwidth-fields with
|
|
206
|
-
* unknown <bwtype> names. The <bwtype> names MUST be alphanumeric and, although no length limit is given,
|
|
207
|
-
* it is recommended that they be short.
|
|
208
|
-
*/
|
|
183
|
+
/** `CT`, `AS`, `TIAS`, `RS`, `RR`, … */
|
|
209
184
|
bwtype: string;
|
|
210
|
-
/**
|
|
211
|
-
* The <bandwidth> is interpreted as kilobits per second by default (including the transport and network-layer,
|
|
212
|
-
* but not the link-layer, overhead). The definition of a new <bwtype> modifier MAY specify that the bandwidth is
|
|
213
|
-
* to be interpreted in some alternative unit (the "CT" and "AS" modifiers defined in this memo use the default
|
|
214
|
-
* units).
|
|
215
|
-
*/
|
|
185
|
+
/** Kilobits per second for CT/AS, bits per second for TIAS */
|
|
216
186
|
bandwidth: number;
|
|
217
187
|
}
|
|
218
|
-
/**
|
|
219
|
-
* The "c=" line (connection-field) contains information necessary to establish a network connection.
|
|
220
|
-
* A session description MUST contain either at least one "c=" line in each media description or a single "c=" line
|
|
221
|
-
* at the session level. It MAY contain a single session-level "c=" line and additional media-level "c=" line(s)
|
|
222
|
-
* per-media-description, in which case the media-level values override the session-level settings for the
|
|
223
|
-
* respective media.
|
|
224
|
-
*/
|
|
188
|
+
/** `c=<nettype> <addrtype> <connection-address>` — RFC 8866 §5.7 */
|
|
225
189
|
interface SdpConnectionInformation {
|
|
190
|
+
netType: string;
|
|
191
|
+
addrType: string;
|
|
192
|
+
/** `<address>[/<ttl>][/<number of addresses>]`, verbatim */
|
|
193
|
+
connectionAddress: string;
|
|
194
|
+
}
|
|
195
|
+
/** `a=group:<semantics> <mid>…` — RFC 5888, RFC 9143 (BUNDLE) */
|
|
196
|
+
interface SdpGroup {
|
|
197
|
+
semantics: 'LS' | 'FID' | 'SRF' | 'ANAT' | 'FEC' | 'DDP' | 'BUNDLE' | (string & {});
|
|
198
|
+
mids: string[];
|
|
199
|
+
}
|
|
200
|
+
/** An RTP payload type with everything attached to it */
|
|
201
|
+
interface SdpCodec {
|
|
202
|
+
payloadType: number;
|
|
203
|
+
/** From `a=rtpmap`; absent for static payload types sent without rtpmap */
|
|
204
|
+
encodingName?: string;
|
|
205
|
+
clockRate?: number;
|
|
206
|
+
/** Number of audio channels, if present in rtpmap */
|
|
207
|
+
encodingParameters?: number;
|
|
226
208
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*/
|
|
230
|
-
netType?: 'IN';
|
|
231
|
-
/**
|
|
232
|
-
* <addrtype> is a text string giving the type of the address that follows. Initially "IP4" and "IP6" are defined,
|
|
233
|
-
* but other values MAY be registered in the future
|
|
234
|
-
*/
|
|
235
|
-
addrType?: 'IP4' | 'IP6';
|
|
236
|
-
/**
|
|
237
|
-
* <base multicast address>[/<ttl>]/<number of addresses>
|
|
209
|
+
* `a=fmtp` parameters in their original order. A parameter without `=` (for example
|
|
210
|
+
* `0-16` for telephone-event) has the value `null`.
|
|
238
211
|
*/
|
|
239
|
-
|
|
212
|
+
parameters?: Record<string, string | null>;
|
|
213
|
+
/** `a=rtcp-fb` values, e.g. `nack`, `nack pli`, `transport-cc` — RFC 4585 */
|
|
214
|
+
rtcpFeedback?: string[];
|
|
240
215
|
}
|
|
241
|
-
interface
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
216
|
+
interface SdpMediaSection extends SdpCommonAttributes {
|
|
217
|
+
/** `m=` media type */
|
|
218
|
+
type: 'audio' | 'video' | 'text' | 'application' | 'message' | (string & {});
|
|
219
|
+
port: number;
|
|
220
|
+
/** `<port>/<number of ports>` */
|
|
221
|
+
numberOfPorts?: number;
|
|
222
|
+
/** Transport protocol, e.g. `UDP/TLS/RTP/SAVPF`, `RTP/AVP`, `UDP/DTLS/SCTP` */
|
|
245
223
|
proto: string;
|
|
246
|
-
|
|
224
|
+
/**
|
|
225
|
+
* The `<fmt>` list of the `m=` line for non-RTP protocols (e.g. `webrtc-datachannel`).
|
|
226
|
+
* For RTP protocols the list is derived from {@link codecs} and this field is absent.
|
|
227
|
+
*/
|
|
228
|
+
formats?: string[];
|
|
229
|
+
/** RTP payload types in `m=` line order. Present for RTP protocols only. */
|
|
230
|
+
codecs?: SdpCodec[];
|
|
231
|
+
/** `a=rtcp-fb:*` values that apply to every payload type */
|
|
232
|
+
rtcpFeedbackWildcard?: string[];
|
|
233
|
+
/** `i=` media title */
|
|
234
|
+
information?: string;
|
|
247
235
|
connectionInformation?: SdpConnectionInformation;
|
|
236
|
+
bandwidths?: SdpBandwidthInformation[];
|
|
237
|
+
encryptionKey?: SdpEncryptionKey;
|
|
238
|
+
/** `a=mid` — RFC 5888 §4 */
|
|
239
|
+
mid?: string;
|
|
240
|
+
/** `a=rtcp` — RFC 3605 */
|
|
241
|
+
rtcp?: SdpRtcp;
|
|
242
|
+
/** `a=rtcp-mux` — RFC 5761 */
|
|
243
|
+
rtcpMux?: boolean;
|
|
244
|
+
/** `a=rtcp-rsize` — RFC 5506 */
|
|
245
|
+
rtcpRsize?: boolean;
|
|
246
|
+
/** `a=sctp-port` — RFC 8841 §5 */
|
|
247
|
+
sctpPort?: number;
|
|
248
|
+
/** `a=max-message-size` — RFC 8841 §6 */
|
|
249
|
+
maxMessageSize?: number;
|
|
250
|
+
/** `a=candidate` — RFC 8839 §5.1 */
|
|
251
|
+
candidates?: SdpCandidate[];
|
|
252
|
+
/** `a=msid` — RFC 8830. Several lines are allowed. */
|
|
253
|
+
msid?: SdpMsid[];
|
|
254
|
+
/** `a=ssrc` — RFC 5576 */
|
|
255
|
+
ssrcs?: SdpSsrc[];
|
|
256
|
+
/** `a=ssrc-group` — RFC 5576 */
|
|
257
|
+
ssrcGroups?: SdpSsrcGroup[];
|
|
258
|
+
/** `a=rid` — RFC 8851 */
|
|
259
|
+
rids?: SdpRid[];
|
|
260
|
+
/** `a=simulcast` — RFC 8853 */
|
|
261
|
+
simulcast?: SdpSimulcast;
|
|
248
262
|
}
|
|
249
|
-
interface
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
263
|
+
interface Sdp extends SdpCommonAttributes {
|
|
264
|
+
/** `v=` — always 0 */
|
|
265
|
+
protocolVersion: number;
|
|
266
|
+
origin: SdpOrigin;
|
|
267
|
+
/** `s=`; a single space if the session has no meaningful name */
|
|
268
|
+
sessionName: string;
|
|
269
|
+
/** `i=` */
|
|
270
|
+
sessionInformation?: string;
|
|
271
|
+
/** `u=` */
|
|
272
|
+
uri?: string;
|
|
273
|
+
/** `e=` lines */
|
|
274
|
+
emailAddresses?: string[];
|
|
275
|
+
/** `p=` lines */
|
|
276
|
+
phoneNumbers?: string[];
|
|
277
|
+
connectionInformation?: SdpConnectionInformation;
|
|
278
|
+
bandwidths?: SdpBandwidthInformation[];
|
|
279
|
+
/** One or more time descriptions */
|
|
280
|
+
timing: SdpTiming[];
|
|
281
|
+
encryptionKey?: SdpEncryptionKey;
|
|
282
|
+
/** `a=group` */
|
|
283
|
+
group?: SdpGroup[];
|
|
284
|
+
/** `a=ice-lite` — RFC 8839 §5.3 */
|
|
285
|
+
iceLite?: boolean;
|
|
286
|
+
media: SdpMediaSection[];
|
|
272
287
|
}
|
|
273
288
|
interface SdpPair {
|
|
274
289
|
offer: Sdp;
|
|
275
290
|
answer: Sdp;
|
|
276
291
|
}
|
|
277
|
-
/**
|
|
278
|
-
* rfc5888 and rfc9143 groups
|
|
279
|
-
*/
|
|
280
|
-
interface SdpGroup {
|
|
281
|
-
/**
|
|
282
|
-
* Semantics Token Reference
|
|
283
|
-
* --------------------------------- ----- -----------
|
|
284
|
-
* Lip Synchronization LS [RFC5888]
|
|
285
|
-
* Flow Identification FID [RFC5888]
|
|
286
|
-
* Single Reservation Flow SRF [RFC3524]
|
|
287
|
-
* Alternative Network Address Types ANAT [RFC4091]
|
|
288
|
-
* Forward Error Correction FEC [RFC4756]
|
|
289
|
-
* Decoding Dependency DDP [RFC5583]
|
|
290
|
-
* Bundle BUNDLE [RFC9143]
|
|
291
|
-
*/
|
|
292
|
-
token: 'LS' | 'FID' | 'SRF' | 'ANAT' | 'FEC' | 'DDP' | 'BUNDLE';
|
|
293
|
-
mids: string[];
|
|
294
|
-
}
|
|
295
292
|
|
|
296
293
|
declare const sdpReader: {
|
|
297
294
|
parse: (sdp: string) => Sdp;
|
|
298
295
|
isValid: (sdp: string, writeLog?: boolean) => boolean;
|
|
299
296
|
};
|
|
300
297
|
|
|
301
|
-
declare const
|
|
298
|
+
declare const sdpWriter: {
|
|
299
|
+
write: (sdp: Sdp) => string;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
type SdpIssueCode = 'parse-error' | 'invalid-version' | 'missing-origin' | 'missing-session-name' | 'empty-session-name' | 'missing-timing' | 'missing-connection' | 'duplicate-mid' | 'bundle-missing-mid' | 'group-unknown-mid' | 'missing-rtpmap' | 'unknown-payload-type' | 'missing-fingerprint' | 'missing-setup' | 'setup-actpass-in-answer' | 'missing-ice-ufrag' | 'missing-ice-pwd' | 'ice-ufrag-length' | 'ice-pwd-length' | 'extmap-invalid-id' | 'extmap-duplicate-id' | 'simulcast-unknown-rid' | 'rid-unknown-payload-type' | 'ssrc-group-unknown-ssrc';
|
|
303
|
+
interface SdpIssue {
|
|
304
|
+
severity: 'error' | 'warning';
|
|
305
|
+
code: SdpIssueCode;
|
|
306
|
+
message: string;
|
|
307
|
+
/** Index in `sdp.media`, if the issue belongs to a media section */
|
|
308
|
+
mediaIndex?: number;
|
|
309
|
+
/** For `parse-error`: the line number reported by the parser (0 if not tied to a line) */
|
|
310
|
+
lineNumber?: number;
|
|
311
|
+
}
|
|
312
|
+
interface SdpValidateOptions {
|
|
313
|
+
/** Enables offer/answer specific checks, e.g. `a=setup:actpass` is not allowed in an answer */
|
|
314
|
+
type?: 'offer' | 'answer';
|
|
315
|
+
}
|
|
316
|
+
declare const sdpValidator: {
|
|
317
|
+
validate: (input: Sdp | string, options?: SdpValidateOptions) => SdpIssue[];
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Thrown by `sdpReader.parse` for a session description that does not follow
|
|
322
|
+
* RFC 8866: a malformed line, an unknown line type, a line out of order, a missing
|
|
323
|
+
* mandatory line, or a malformed value of a typed attribute.
|
|
324
|
+
*/
|
|
325
|
+
declare class SdpParseError extends Error {
|
|
326
|
+
/** The error description without the line reference */
|
|
327
|
+
readonly reason: string;
|
|
328
|
+
/** 1-based line number, or 0 when the error is not tied to a line (e.g. a missing line) */
|
|
329
|
+
readonly lineNumber: number;
|
|
330
|
+
/** The offending line, without the line terminator */
|
|
331
|
+
readonly line: string;
|
|
332
|
+
constructor(reason: string, lineNumber: number, line: string);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
declare const version = "1.0.0";
|
|
302
336
|
|
|
303
|
-
export { Sdp, SdpEncryptionKey, SdpMediaSection, SdpOrigin, SdpPair, SdpRepeatTimes, SdpTiming, sdpReader, version };
|
|
337
|
+
export { Sdp, SdpBandwidthInformation, SdpCandidate, SdpCodec, SdpCommonAttributes, SdpConnectionInformation, SdpDirection, SdpEncryptionKey, SdpExtmap, SdpFingerprint, SdpGenericAttribute, SdpGroup, SdpIssue, SdpIssueCode, SdpMediaSection, SdpMsid, SdpOrigin, SdpPair, SdpParseError, SdpRepeatTimes, SdpRid, SdpRtcp, SdpSetup, SdpSimulcast, SdpSimulcastRid, SdpSimulcastStreams, SdpSsrc, SdpSsrcGroup, SdpTimeZoneAdjustment, SdpTiming, SdpValidateOptions, sdpReader, sdpValidator, sdpWriter, version };
|