@irbisadm/simple-sdp 0.1.0 → 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 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]}},c=(e,t,r)=>{e.ice||(e.ice={ufrag:"",pwd:"",options:[]}),e.ice[t]=r},p=(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":c(e,"ufrag",a);break;case"ice-pwd":c(e,"pwd",a);break;case"ice-options":c(e,"options",a.split(" "));break;case"fingerprint":{const[t,r]=a.split(" ");p(e,"algorithm",t),p(e,"fingerprint",r);break}case"setup":p(e,"setup",a);break;case"sendrecv":e.direction=s;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 c=null;return r.forEach(((t,p)=>{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);c=r,n.media||(n.media=[]),n.media.push(r);break;case"b":c?c.bandwidthInformation=i(t):n.bandwidthInformation=i(t);break;case"c":c?c.connectionInformation=o(t):n.connectionInformation=o(t);break;case"a":l(c||n,t);break;default:throw Error(`Unknown line type "${t[0]}"`)}}else if(p!==r.length-1)throw new Error(`Empty line at "${p}" after ${r[p-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.0";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,42 +1,67 @@
1
- {
2
- "name": "@irbisadm/simple-sdp",
3
- "version": "0.1.0",
4
- "license": "MIT",
5
- "description": "WebRTC SDP parser and validator influenced by pjsip library.",
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
- "files": [
18
- "./dist/index.mjs",
19
- "./types/index.d.ts",
20
- "./readme.md"
21
- ],
22
- "devDependencies": {
23
- "@rollup/plugin-replace": "^5.0.2",
24
- "@rollup/plugin-terser": "^0.4.0",
25
- "@rollup/plugin-typescript": "^11.0.0",
26
- "@types/jest": "^29.5.0",
27
- "@types/node": "^18.14.0",
28
- "jest": "^29.5.0",
29
- "rollup": "^3.17.1",
30
- "rollup-plugin-dts": "^5.2.0",
31
- "ts-jest": "^29.0.5",
32
- "tslib": "^2.5.0",
33
- "typescript": "^5.0.2"
34
- },
35
- "engines": {
36
- "node": ">=17.0.0"
37
- },
38
- "scripts": {
39
- "build": "rollup -c",
40
- "test": "jest"
41
- }
42
- }
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
- ![license MIT](https://badgen.net/npm/license/@irbisadm/simple-sdp)
2
- ![license MIT](https://badgen.net/npm/v/@irbisadm/simple-sdp)
3
- ![license MIT](https://badgen.net/npm/types/@irbisadm/simple-sdp)
4
- ![license MIT](https://badgen.net/bundlephobia/minzip/@irbisadm/simple-sdp)
5
- ![license MIT](https://badgen.net/bundlephobia/dependency-count/@irbisadm/simple-sdp)
6
- ![license MIT](https://badgen.net/bundlephobia/tree-shaking/@irbisadm/simple-sdp)
7
- # Simple SDP tools
8
- This project is a TypeScript library that provides a comprehensive set of tools for working with Session Description Protocol (SDP) messages in WebRTC applications. SDP is a text-based format that describes the multimedia communication sessions between peers.
9
- With this library, you can:
10
- - Parse and generate SDP messages from and to TypeScript objects
11
- - Manipulate SDP data by adding, removing, or modifying media streams, codecs, candidates, and other attributes
12
- - Validate SDP messages according to the [RFC 4566](https://www.rfc-editor.org/rfc/rfc8866) specification and WebRTC best practices
13
- - Track and verify the SDP history for a single PeerConnection instance
14
-
15
- This project aims to help WebRTC beginners learn the SDP format and its components, as well as provide a clear and easy-to-use TypeScript API for advanced users. This project was inspired by the [pjsip](https://github.com/pjsip/pjproject) library written in C++.
16
-
17
- You can use the library with Node.js as well in any modern web browser.
18
-
19
- ## SIP disclaimer
20
- Probably this library can be used for parsing and creating SDP for SIP.
21
- I can see a theoretical problems here because some SIP devices are complained only with older SDP RFC: [RFC 4566](https://www.rfc-editor.org/rfc/rfc4566.html) or [RFC 2327](https://www.rfc-editor.org/rfc/rfc2327).
22
-
23
- I'm not an expert in SIP and have no test environment to check compatibility with that standards. If you can help with tests or expertise with SIP, please send me a message to [i@irbisadm.dev](mailto:i@irbisadm.dev)
1
+ ![license MIT](https://badgen.net/npm/license/@irbisadm/simple-sdp)
2
+ ![npm version](https://badgen.net/npm/v/@irbisadm/simple-sdp)
3
+ ![npm types](https://badgen.net/npm/types/@irbisadm/simple-sdp)
4
+ ![bundlephobia min-zip size](https://badgen.net/bundlephobia/minzip/@irbisadm/simple-sdp)
5
+ ![bundlephobia dependency count](https://badgen.net/bundlephobia/dependency-count/@irbisadm/simple-sdp)
6
+ ![CI](https://github.com/irbisadm/simple-sdp/actions/workflows/ci.yml/badge.svg)
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,273 +1,289 @@
1
- interface SdpCommonAttributes {
2
- attributes?: Record<string, SdpAttribute>;
3
- ice?: SdpIceAttributes;
4
- dtls?: SdpDtlsAttribute;
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
- interface Sdp extends SdpCommonAttributes {
7
- protocolVersion?: number;
8
- /**
9
- * The "o=" field gives the originator of the session (her username and the address of the user's host) plus a session
10
- * identifier and version number:
11
- */
12
- origin?: SdpOrigin;
13
- /**
14
- * The "s=" field is the textual session name. There MUST be one and only one "s=" field per session description. The
15
- * "s=" field MUST NOT be empty and SHOULD contain ISO 10646 characters (but see also the "a=charset" attribute).
16
- * If a session has no meaningful name, the value "s= " SHOULD be used (i.e., a single space as the session name).
17
- */
18
- sessionName?: string;
19
- /**
20
- *
21
- * The "i=" field provides textual information about the session. There MUST be at most one session-level "i="
22
- * field per session description, and at most one "i=" field per media. If the "a=charset" attribute is present,
23
- * it specifies the character set used in the "i=" field. If the "a=charset" attribute is not present, the "i="
24
- * field MUST contain ISO 10646 characters in UTF-8 encoding.
25
- * A single "i=" field MAY also be used for each media definition. In media definitions, "i=" fields are
26
- * primarily intended for labelling media streams. As such, they are most likely to be useful when a single
27
- * session has more than one distinct media stream of the same media type. An example would be two different
28
- * whiteboards, one for slides and one for feedback and questions. The "i=" field is intended to provide a
29
- * free-form human-readable description of the session or the purpose of a media stream. It is not suitable
30
- * for parsing by automata.
31
- */
32
- sessionInformation?: string;
33
- /**
34
- * A URI is a Uniform Resource Identifier as used by WWW clients. The URI should be a pointer to additional
35
- * information about the session. This field is OPTIONAL, but if it is present it MUST be specified before the
36
- * first media field. No more than one URI field is allowed per session description.
37
- */
38
- uri?: string;
39
- emailAddress?: string;
40
- phoneNumber?: string;
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;
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>;
61
55
  }
62
56
  /**
63
- * The "o=" field gives the originator of the session (her username and the address of the user's host) plus a session
64
- * identifier and version number:
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.
65
59
  */
66
- interface SdpOrigin {
67
- /**
68
- * <username> is the user's login on the originating host, or it is "-" if the originating host does not support
69
- * the concept of user IDs. The <username> MUST NOT contain spaces.
70
- */
71
- username?: string;
72
- /**
73
- * <sess-id> is a numeric string such that the tuple of <username>, <sess-id>, <nettype>, <addrtype>, and
74
- * <unicast-address> forms a globally unique identifier for the session. The method of <sess-id> allocation is up
75
- * to the creating tool, but it has been suggested that a Network Time Protocol (NTP) format timestamp be used to
76
- * ensure uniqueness
77
- */
78
- sessId?: string;
79
- /**
80
- * <sess-version> is a version number for this session description. Its usage is up to the creating tool, so long as
81
- * <sess-version> is increased when a modification is made to the session data. Again, it is RECOMMENDED that an NTP
82
- * format timestamp is used.
83
- */
84
- sessVersion?: number;
85
- /**
86
- * <nettype> is a text string giving the type of network. Initially "IN" is defined to have the meaning "Internet",
87
- * but other values MAY be registered in the future.
88
- */
89
- netType?: 'IN';
90
- /**
91
- * <addrtype> is a text string giving the type of the address that follows. Initially "IP4" and "IP6" are defined,
92
- * but other values MAY be registered in the future
93
- */
94
- 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[];
95
84
  /**
96
- * <unicast-address> is the address of the machine from which the session was created. For an address type of IP4,
97
- * this is either the fully qualified domain name of the machine or the dotted-decimal representation of the
98
- * IP version 4 address of the machine. For an address type of IP6, this is either the fully qualified domain name of
99
- * the machine or the compressed textual representation of the IP version 6 address of the machine. For both IP4
100
- * and IP6, the fully qualified domain name is the form that SHOULD be given unless this is unavailable, in which case
101
- * the globally unique address MAY be substituted. A local IP address MUST NOT be used in any context where the SDP
102
- * description might leave the scope in which the address is meaningful (for example, a local address MUST NOT be
103
- * 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.
104
87
  */
105
- unicastAddress: string;
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;
106
95
  }
107
96
  /**
108
- * The "t=" lines specify the start and stop times for a session. Multiple "t=" lines MAY be used if a session is
109
- * active at multiple irregularly spaced times; each additional "t=" line specifies an additional period of time for
110
- * which the session will be active. If the session is active at regular times, an "r=" line (see below) should be
111
- * used in addition to, and following, a "t=" line -- in which case the "t=" line specifies the start and stop times
112
- * of the repeat sequence.
97
+ * Simulcast streams of one direction. Every element is one simulcast stream (`;`-separated),
98
+ * listing its alternative rids (`,`-separated).
113
99
  */
114
- interface SdpTiming {
115
- startTime: number;
116
- stopTime: number;
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[];
117
135
  }
118
136
  /**
119
- * "r=" fields specify repeat times for a session. For example, if a session is active at 10am on Monday and 11am on
120
- * Tuesday for one hour each week for three months, then the <start-time> in the corresponding "t=" field would be the
121
- * NTP representation of 10am on the first Monday, the <repeat interval> would be 1 week, the <active duration> would
122
- * be 1 hour, and the offsets would be zero and 25 hours. The corresponding "t=" field stop time would be the NTP
123
- * 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
124
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. */
125
156
  interface SdpRepeatTimes {
126
157
  repeatInterval: number;
127
158
  activeDuration: number;
128
- offsets?: number[];
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;
129
165
  }
130
166
  /**
131
- * If transported over a secure and trusted channel, the Session Description Protocol MAY be used to convey encryption
132
- * keys. A simple mechanism for key exchange is provided by the key field ("k="), although this is primarily
133
- * supported for compatibility with older implementations and its use is NOT RECOMMENDED. Work is in progress to define
134
- * new key exchange mechanisms for use with SDP [27] [28], and it is expected that new applications will use
135
- * those mechanisms.
136
- *
137
- * A key field is permitted before the first media entry (in which case it applies to all media in the session), or
138
- * for each media entry as required. The format of keys and their usage are outside the scope of this document, and
139
- * the key field provides no way to indicate the encryption algorithm to be used, key type, or other information about
140
- * the key: this is assumed to be provided by the higher-level protocol using SDP. If there is a need to convey this
141
- * information within SDP, the extensions mentioned previously SHOULD be used. Many security protocols require two
142
- * 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.
143
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) */
144
177
  interface SdpEncryptionKey {
145
- /**
146
- * The method indicates the mechanism to be used to obtain a usable key by external means, or from the encoded
147
- * encryption key given. The following methods are defined:
148
- *
149
- * clear
150
- * The encryption key is included untransformed in this key field. This method MUST NOT be used unless it can be
151
- * guaranteed that the SDP is conveyed over a secure channel. The encryption key is interpreted as text according
152
- * to the charset attribute; use the "k=base64:" method to convey characters that are otherwise prohibited in SDP.
153
- *
154
- * base64
155
- * The encryption key is included in this key field but has been base64 encoded because it includes characters
156
- * that are prohibited in SDP. This method MUST NOT be used unless it can be guaranteed that the SDP is
157
- * conveyed over a secure channel.
158
- *
159
- * uri
160
- * A Uniform Resource Identifier is included in the key field. The URI refers to the data containing the key,
161
- * and may require additional authentication before the key can be returned. When a request is made to the given
162
- * URI, the reply should specify the encoding for the key. The URI is often an Secure Socket Layer/Transport
163
- * Layer Security (SSL/TLS)-protected HTTP URI ("https:"), although this is not required.
164
- *
165
- * prompt
166
- * No key is included in this SDP description, but the session or media stream referred to by this key field
167
- * is encrypted. The user should be prompted for the key when attempting to join the session, and this
168
- * user-supplied key should then be used to decrypt the media streams. The use of user-specified keys is
169
- * NOT RECOMMENDED, since such keys tend to have weak security properties.
170
- */
171
- method: 'clear' | 'base64' | 'uri' | 'prompt';
178
+ method: 'clear' | 'base64' | 'uri' | 'prompt' | (string & {});
172
179
  encryptionKey?: string;
173
180
  }
174
- /**
175
- * The OPTIONAL "b=" line (bandwidth-field) denotes the proposed bandwidth to be used by the session or media
176
- * description. The <bwtype> is an alphanumeric modifier that provides the meaning of the <bandwidth> number. Two
177
- * values are defined in this specification, but other values MAY be registered in the future
178
- * (see Section 8 and [RFC3556], [RFC3890]):
179
- */
181
+ /** `b=<bwtype>:<bandwidth>` — RFC 8866 §5.8, RFC 3890 (TIAS) */
180
182
  interface SdpBandwidthInformation {
181
- /**
182
- * CT
183
- * If the bandwidth of a session is different from the bandwidth implicit from the scope, a "b=CT:" line SHOULD
184
- * be supplied for the session giving the proposed upper limit to the bandwidth used
185
- * (the "conference total" bandwidth). Similarly, if the bandwidth of bundled media streams [RFC8843] in
186
- * an "m=" line is different from the implicit value from the scope, a "b=CT:" line SHOULD be supplied in the
187
- * media level. The primary purpose of this is to give an approximate idea as to whether two or more sessions
188
- * (or bundled media streams) can coexist simultaneously. Note that a "b=CT:" line gives a total bandwidth figure
189
- * for all the media at all endpoints.
190
- *
191
- * The Mux Category for "b=CT:" is NORMAL. This is discussed in [RFC8859].
192
- *
193
- * AS
194
- * The bandwidth is interpreted to be application specific (it will be the application's concept
195
- * of maximum bandwidth). Normally, this will coincide with what is set on the application's "maximum bandwidth"
196
- * control if applicable. For RTP-based applications, the "b=AS:" line gives the RTP "session bandwidth" as defined
197
- * in Section 6.2 of [RFC3550]. Note that a "b=AS:" line gives a bandwidth figure for a single media at
198
- * a single endpoint, although there may be many endpoints sending simultaneously.
199
- *
200
- * The Mux Category for "b=AS:" is SUM. This is discussed in [RFC8859].
201
- *
202
- * [RFC4566] defined an "X-" prefix for <bwtype> names. This was intended for experimental purposes only.
203
- * Use of the "X-" prefix is NOT RECOMMENDED. Instead new (non "X-" prefix) <bwtype> names SHOULD be defined,
204
- * and then MUST be registered with IANA in the standard namespace. SDP parsers MUST ignore bandwidth-fields with
205
- * unknown <bwtype> names. The <bwtype> names MUST be alphanumeric and, although no length limit is given,
206
- * it is recommended that they be short.
207
- */
183
+ /** `CT`, `AS`, `TIAS`, `RS`, `RR`, … */
208
184
  bwtype: string;
209
- /**
210
- * The <bandwidth> is interpreted as kilobits per second by default (including the transport and network-layer,
211
- * but not the link-layer, overhead). The definition of a new <bwtype> modifier MAY specify that the bandwidth is
212
- * to be interpreted in some alternative unit (the "CT" and "AS" modifiers defined in this memo use the default
213
- * units).
214
- */
185
+ /** Kilobits per second for CT/AS, bits per second for TIAS */
215
186
  bandwidth: number;
216
187
  }
217
- /**
218
- * The "c=" line (connection-field) contains information necessary to establish a network connection.
219
- * A session description MUST contain either at least one "c=" line in each media description or a single "c=" line
220
- * at the session level. It MAY contain a single session-level "c=" line and additional media-level "c=" line(s)
221
- * per-media-description, in which case the media-level values override the session-level settings for the
222
- * respective media.
223
- */
188
+ /** `c=<nettype> <addrtype> <connection-address>` — RFC 8866 §5.7 */
224
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;
225
208
  /**
226
- * <nettype> is a text string giving the type of network. Initially "IN" is defined to have the meaning "Internet",
227
- * but other values MAY be registered in the future.
228
- */
229
- netType?: 'IN';
230
- /**
231
- * <addrtype> is a text string giving the type of the address that follows. Initially "IP4" and "IP6" are defined,
232
- * but other values MAY be registered in the future
233
- */
234
- addrType?: 'IP4' | 'IP6';
235
- /**
236
- * <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`.
237
211
  */
238
- connectionAddress: string;
212
+ parameters?: Record<string, string | null>;
213
+ /** `a=rtcp-fb` values, e.g. `nack`, `nack pli`, `transport-cc` — RFC 4585 */
214
+ rtcpFeedback?: string[];
239
215
  }
240
- interface SdpBaseMediaSection extends SdpCommonAttributes {
241
- mid?: string;
242
- direction?: 'sendrecv' | 'sendonly' | 'recvonly' | 'inactive';
243
- ports: number[];
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` */
244
223
  proto: string;
245
- bandwidthInformation?: SdpBandwidthInformation;
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;
246
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;
247
262
  }
248
- interface SdpAVMediaSection extends SdpBaseMediaSection {
249
- type: 'audio' | 'video';
250
- fmtp?: number[];
251
- }
252
- interface SdpAppMediaSection extends SdpBaseMediaSection {
253
- type: 'application';
254
- description?: string;
255
- }
256
- interface SdpOtherMediaSection extends SdpBaseMediaSection {
257
- type: 'text' | 'message' | string;
258
- fmtp?: string[];
259
- }
260
- type SdpMediaSection = SdpAVMediaSection | SdpAppMediaSection | SdpOtherMediaSection;
261
- type SdpAttribute = string | boolean | string[];
262
- interface SdpIceAttributes {
263
- ufrag: string;
264
- pwd: string;
265
- options: string[];
266
- }
267
- interface SdpDtlsAttribute {
268
- algorithm: string;
269
- fingerprint: string;
270
- setup: 'active' | 'passive' | 'actpass';
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[];
271
287
  }
272
288
  interface SdpPair {
273
289
  offer: Sdp;
@@ -279,6 +295,43 @@ declare const sdpReader: {
279
295
  isValid: (sdp: string, writeLog?: boolean) => boolean;
280
296
  };
281
297
 
282
- declare const version = "0.1.0";
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";
283
336
 
284
- 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 };