@irbisadm/simple-sdp 1.0.1 → 1.2.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 +29 -1
- package/dist/index.mjs +1 -1
- package/package.json +20 -4
- package/readme.md +73 -0
- package/types/index.d.ts +1 -1
- package/wasm/dist/index.d.ts +2 -0
- package/wasm/dist/index.mjs +51 -0
- package/wasm/dist/simple-sdp.mjs +144 -0
- package/wasm/dist/simple-sdp.wasm +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,35 @@
|
|
|
3
3
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The project
|
|
4
4
|
follows [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
-
## [1.0
|
|
6
|
+
## [1.2.0] — 2026-11-01
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- `@irbisadm/simple-sdp/wasm`: the same API and types as the main entry point, run by the
|
|
10
|
+
WebAssembly module (ESM with top-level await; works in Node.js and with bundlers that
|
|
11
|
+
handle `new URL(…, import.meta.url)`, checked with Vite 6). The module itself is
|
|
12
|
+
exported as `@irbisadm/simple-sdp/wasm/simple-sdp.wasm`.
|
|
13
|
+
- Kotlin library on JitPack: `com.github.irbisadm:simple-sdp:v1.2.0` (`jitpack.yml`).
|
|
14
|
+
- GitHub Releases carry `simple-sdp.wasm` for other runtimes (`.github/workflows/release.yml`).
|
|
15
|
+
|
|
16
|
+
## [1.1.0] — 2026-11-01
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- AssemblyScript port (`assembly/`): the parser, writer and validator compiled to a
|
|
20
|
+
WebAssembly module with no imports (`yarn asbuild`). Results are JSON with the shape of
|
|
21
|
+
the TypeScript model; a C-style interface on UTF-8 buffers (`sdp_parse`, `sdp_write`,
|
|
22
|
+
`sdp_validate`, `sdp_validate_model`, …) serves non-JS hosts. `yarn test:wasm` runs the
|
|
23
|
+
test suites against the module, plus a differential test against the TypeScript code.
|
|
24
|
+
- Swift package `SimpleSdp` for iOS 18+ / macOS 15+ (WasmKit), `Package.swift` in the root.
|
|
25
|
+
- Kotlin library `dev.irbisadm:simple-sdp` for Android and the JVM (Chicory; the module is
|
|
26
|
+
compiled to bytecode at build time), `platforms/kotlin`.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- Attributes named like `Object.prototype` members (`a=constructor`, `a=toString`,
|
|
30
|
+
`a=__proto__`, …) failed to parse; an ssrc attribute with such a name was dropped, a
|
|
31
|
+
`__proto__` key of fmtp/ssrc/candidate records changed the object prototype, and
|
|
32
|
+
`a=fingerprint:constructor …` got a validator message with "function Object()".
|
|
33
|
+
|
|
34
|
+
## [1.0.1] — 2026-11-01
|
|
7
35
|
|
|
8
36
|
### Fixed
|
|
9
37
|
- Validator accepted malformed `a=fingerprint` values such as `sha-256 AA:BB`.
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
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},$='"<foundation> <component> <transport> <priority> <address> <port> typ <type> [raddr <addr>] [rport <port>] *(<ext-name> <ext-value>)"',w=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 ${$}`);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 ${$}`);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=w(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(w)}})(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)}}},F=(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})},P={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},z=new Set(["e","p","b","a"]),I={m:0,i:1,c:2,b:3,k:4,a:5},C=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"},O=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?C:z,r=(l?I:P)[x];void 0===r&&b(x in P?`"${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!==P.t&&u!==P.r&&b(`"${x}=" must follow "t=" or "r="`):u>P.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":F(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!==($=p.timing)&&void 0!==$?$:p.timing=[]).push(m);break;case"r":(null!==(w=(y=m).repeat)&&void 0!==w?w: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":F(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},A={parse:O,isValid:(e,t=!1)=>{try{return O(e),!0}catch(e){return t&&console.error(e),!1}}},N=e=>`c=${e.netType} ${e.addrType} ${e.connectionAddress}`,S=e=>`b=${e.bwtype}:${e.bandwidth}`,U=e=>`k=${e.method}${void 0!==e.encryptionKey?`:${e.encryptionKey}`:""}`,M=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} ${M(e.parameters)}`]:[]]},V=e=>{const t=[...e.payloadTypes?[`pt=${e.payloadTypes.join(",")}`]:[],...e.restrictions&&Object.keys(e.restrictions).length?[M(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?[N(e.connectionInformation)]:[],...(null!==(t=e.bandwidths)&&void 0!==t?t:[]).map(S),...e.encryptionKey?[U(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?[N(e.connectionInformation)]:[],...(null!==(o=e.bandwidths)&&void 0!==o?o:[]).map(S),...e.timing.flatMap(q),...e.encryptionKey?[U(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"}},_={md2:16,md5:16,"sha-1":20,"sha-224":28,"sha-256":32,"sha-384":48,"sha-512":64},H=/^[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2})*$/,J={validate:(t,r={})=>{var i,s,n,o;let d;if("string"==typeof t)try{d=A.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)};v(d.iceUfrag,d.icePwd);const h=(e,t)=>{var r;for(const{hashFunction:i,fingerprint:s}of null!==(r=e.fingerprints)&&void 0!==r?r:[]){const e=_[i.toLowerCase()];H.test(s)?void 0!==e&&s.split(":").length!==e?p("invalid-fingerprint",`a=fingerprint:${i} has ${s.split(":").length} bytes, expected ${e} (RFC 8122 §5)`,t):s!==s.toUpperCase()&&l("fingerprint-not-uppercase",`a=fingerprint:${i} must use upper-case hex (RFC 8122 §5)`,t):p("invalid-fingerprint",`a=fingerprint:${i} must be colon-separated hex pairs (RFC 8122 §5)`,t)}};return h(d),m.forEach(((e,t)=>{var i,s,n,o,c,m,u,g,$,w,y,b,x,k,E,T;const j=0!==e.port;if(v(e.iceUfrag,e.icePwd,t),h(e,t),f&&j&&(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)&&j){(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 F=new Set;for(const r of null!==(g=e.extmap)&&void 0!==g?g:[])(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),F.has(r.id)&&p("extmap-duplicate-id",`extmap id ${r.id} is used more than once`,t),F.add(r.id);for(const r of["send","recv"])for(const i of(null!==(w=null===($=e.simulcast)||void 0===$?void 0:$[r])&&void 0!==w?w:[]).flat())(null===(y=e.rids)||void 0===y?void 0:y.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!==(b=e.rids)&&void 0!==b?b:[])for(const i of null!==(x=r.payloadTypes)&&void 0!==x?x:[])(null===(k=e.codecs)||void 0===k?void 0:k.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!==(E=e.ssrcGroups)&&void 0!==E?E:[])for(const i of r.ssrcs)(null===(T=e.ssrcs)||void 0===T?void 0:T.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}},Q="1.0.1";export{e as SdpParseError,A as sdpReader,J as sdpValidator,Z as sdpWriter,Q 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)=>Object.prototype.hasOwnProperty.call(e,t)?e[t]:void 0,l=(e,t,r)=>{Object.defineProperty(e,t,{value:r,writable:!0,enumerable:!0,configurable:!0})},m=(e,t)=>{if(void 0===e||0===e.length)throw new Error(`expected a value: ${t}`);return e},u=e=>{if(void 0!==e)throw new Error("flag attribute must not have a value");return!0},f=e=>{const t=e.indexOf(" ");return-1===t?[e,void 0]:[e.slice(0,t),e.slice(t+1)]},v=(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))},h=(e,t,r,i)=>{if(void 0!==e[t])throw new Error(`duplicate a=${i}`);e[t]=r},g=(e,t,r)=>{void 0===e[t]&&(e[t]=[]),e[t].push(r)},$=["sendrecv","sendonly","recvonly","inactive"],w=e=>{const t={};for(const r of e.split(";")){const e=r.trim();if(0===e.length)continue;const i=e.indexOf("=");-1===i?l(t,e,null):l(t,e.slice(0,i),e.slice(i+1))}return t},y='"<foundation> <component> <transport> <priority> <address> <port> typ <type> [raddr <addr>] [rport <port>] *(<ext-name> <ext-value>)"',b=e=>{const t=i(e,"ssrc-id");if(t>4294967295)throw new Error(`ssrc-id ${t} does not fit into 32 bits`);return t},x=/^[A-Za-z0-9_-]+$/,k=["session","media"],E=["media"],T=["session"],j=["active","passive","actpass","holdconn"],P=e=>({scopes:k,parse:t=>h(t,"direction",e,e)}),F={sendrecv:P("sendrecv"),sendonly:P("sendonly"),recvonly:P("recvonly"),inactive:P("inactive"),"ice-ufrag":{scopes:k,parse:(e,t)=>h(e,"iceUfrag",m(t,"a=ice-ufrag:<ufrag>"),"ice-ufrag")},"ice-pwd":{scopes:k,parse:(e,t)=>h(e,"icePwd",m(t,"a=ice-pwd:<password>"),"ice-pwd")},"ice-options":{scopes:k,parse:(e,t)=>h(e,"iceOptions",m(t,"a=ice-options:<option> ...").split(" "),"ice-options")},"ice-lite":{scopes:T,parse:(e,t)=>h(e,"iceLite",u(t),"ice-lite")},candidate:{scopes:E,parse:(e,t)=>g(e,"candidates",(e=>{var t;const r=e.split(" ");if(r.length<8||"typ"!==r[6]||r.length%2!=0)throw new Error(`expected ${y}`);const[s,n,o,a,d,c,,p]=r,m={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 ${y}`);switch(s){case"raddr":m.relatedAddress=n;break;case"rport":m.relatedPort=i(n,"rport");break;case"tcptype":m.tcpType=n;break;default:l(null!==(t=m.extensions)&&void 0!==t?t:m.extensions={},s,n)}}return m})(m(t,"a=candidate:<candidate>")))},"end-of-candidates":{scopes:k,parse:(e,t)=>h(e,"endOfCandidates",u(t),"end-of-candidates")},fingerprint:{scopes:k,parse:(e,t)=>{const[r,i]=f(m(t,"a=fingerprint:<hash-function> <fingerprint>"));if(!i||i.includes(" "))throw new Error('expected "a=fingerprint:<hash-function> <fingerprint>"');g(e,"fingerprints",{hashFunction:r,fingerprint:i})}},setup:{scopes:k,parse:(e,t)=>{if(!j.includes(t))throw new Error(`a=setup must be one of ${j.join(", ")}`);h(e,"setup",t,"setup")}},extmap:{scopes:k,parse:(e,t)=>g(e,"extmap",(e=>{const[t,r]=f(e);if(!r)throw new Error('expected "<id>[/<direction>] <uri> [<attributes>]"');const[s,n,...o]=t.split("/");if(o.length||void 0!==n&&!$.includes(n))throw new Error(`invalid extmap id "${t}"`);const[a,d]=f(r),c={id:i(s,"extmap id"),uri:a};return void 0!==n&&(c.direction=n),void 0!==d&&(c.attributes=d),c})(m(t,"a=extmap:<id> <uri>")))},group:{scopes:T,parse:(e,t)=>g(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}})(m(t,"a=group:<semantics> <mid> ...")))},mid:{scopes:E,parse:(e,t)=>h(e,"mid",m(t,"a=mid:<identification-tag>"),"mid")},msid:{scopes:E,parse:(e,t)=>g(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}})(m(t,"a=msid:<stream id> [<track id>]")))},ssrc:{scopes:E,parse:(e,t)=>((e,t)=>{var r;const[i,s]=f(t);if(!s)throw new Error('expected "<ssrc-id> <attribute>[:<value>]"');const n=b(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 m=c.find((e=>e.id===n));return m||(m={id:n,attributes:{}},c.push(m)),void 0===p(m.attributes,a)&&(l(m.attributes,a,d),!0)})(e,m(t,"a=ssrc:<ssrc-id> <attribute>[:<value>]"))},"ssrc-group":{scopes:E,parse:(e,t)=>g(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(b)}})(m(t,"a=ssrc-group:<semantics> <ssrc-id> ...")))},rid:{scopes:E,parse:(e,t)=>g(e,"rids",(e=>{const[t,r]=f(e),[s,n]=r?f(r):[void 0,void 0];if(!x.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=w(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})(m(t,"a=rid:<rid-id> <send|recv>")))},simulcast:{scopes:E,parse:(e,t)=>h(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(!x.test(r))throw new Error(`invalid simulcast rid "${e}"`);return t?{rid:r,paused:t}:{rid:r}}))))}return r})(m(t,"a=simulcast:<send|recv> <streams>")),"simulcast")},"extmap-allow-mixed":{scopes:k,parse:(e,t)=>h(e,"extmapAllowMixed",u(t),"extmap-allow-mixed")},"rtcp-mux":{scopes:E,parse:(e,t)=>h(e,"rtcpMux",u(t),"rtcp-mux")},"rtcp-rsize":{scopes:E,parse:(e,t)=>h(e,"rtcpRsize",u(t),"rtcp-rsize")},rtcp:{scopes:E,parse:(e,t)=>{const r=m(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;h(e,"rtcp",1===r.length?{port:i(s,"port")}:{port:i(s,"port"),netType:n,addrType:o,address:a},"rtcp")}},"sctp-port":{scopes:E,parse:(e,t)=>h(e,"sctpPort",i(m(t,"a=sctp-port:<port>"),"sctp-port"),"sctp-port")},"max-message-size":{scopes:E,parse:(e,t)=>h(e,"maxMessageSize",i(m(t,"a=max-message-size:<size>"),"max-message-size"),"max-message-size")},rtpmap:{scopes:E,parse:(e,t)=>{const[r,s]=f(m(t,"a=rtpmap:<payload type> <encoding>")),n=e.codecs?v(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:E,parse:(e,t)=>{const[r,i]=f(m(t,"a=fmtp:<format> <parameters>")),s=e.codecs?v(e,r):void 0;if(!s||void 0!==s.parameters||void 0===i)return!1;s.parameters=w(i)}},"rtcp-fb":{scopes:E,parse:(e,t)=>{const[r,i]=f(m(t,"a=rtcp-fb:<payload type> <feedback>"));if(!i||!e.codecs)return!1;if("*"===r)return void g(e,"rtcpFeedbackWildcard",i);const s=v(e,r);if(!s)return!1;g(s,"rtcpFeedback",i)}}},z=(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=p(F,s);o&&o.scopes.includes(t)&&!1!==o.parse(e,n)||g(e,"attributes",void 0===n?{name:s}:{name:s,value:n})},I={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},O=new Set(["e","p","b","a"]),C={m:0,i:1,c:2,b:3,k:4,a:5},R=new Set(["b","a"]),A={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?R:O,r=(l?C:I)[x];void 0===r&&b(x in I?`"${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: ${A[e]})`):r!==u||t.has(x)||b(l&&"c"===x?'multiple "c=" lines in one media description are not supported':`duplicate "${x}=" line`):u!==I.t&&u!==I.r&&b(`"${x}=" must follow "t=" or "r="`):u>I.z&&b(`"t=" must precede "k=" and "a=" (order: ${A.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":z(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!==($=p.timing)&&void 0!==$?$:p.timing=[]).push(m);break;case"r":(null!==(w=(y=m).repeat)&&void 0!==w?w: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":z(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},S={parse:N,isValid:(e,t=!1)=>{try{return N(e),!0}catch(e){return t&&console.error(e),!1}}},U=e=>`c=${e.netType} ${e.addrType} ${e.connectionAddress}`,M=e=>`b=${e.bwtype}:${e.bandwidth}`,q=e=>`k=${e.method}${void 0!==e.encryptionKey?`:${e.encryptionKey}`:""}`,K=e=>Object.entries(e).map((([e,t])=>null===t?e:`${e}=${t}`)).join(";"),V=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(" ")}`]:[]]},D=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} ${K(e.parameters)}`]:[]]},L=e=>{const t=[...e.payloadTypes?[`pt=${e.payloadTypes.join(",")}`]:[],...e.restrictions&&Object.keys(e.restrictions).length?[K(e.restrictions)]:[]].join(";");return`a=rid:${e.id} ${e.direction}${t?` ${t}`:""}`},W=(e,t)=>e?[`a=${t}`]:[],G=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}`]:[]]},B=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}`:""}`)),...W(e.extmapAllowMixed,"extmap-allow-mixed")]},Z=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}`))},_=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?[U(e.connectionInformation)]:[],...(null!==(t=e.bandwidths)&&void 0!==t?t:[]).map(M),...e.encryptionKey?[q(e.encryptionKey)]:[],...e.rtcp?[`a=rtcp:${e.rtcp.port}${void 0!==e.rtcp.netType?` ${e.rtcp.netType} ${e.rtcp.addrType} ${e.rtcp.address}`:""}`]:[],...G(e),...void 0!==e.mid?[`a=mid:${e.mid}`]:[],...B(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}`:""}`)),...W(e.rtcpMux,"rtcp-mux"),...W(e.rtcpRsize,"rtcp-rsize"),...(null!==(i=e.codecs)&&void 0!==i?i:[]).flatMap(D),...(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(L),...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)}`)),...W(e.endOfCandidates,"end-of-candidates"),...Z(e)];var p},H={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?[U(e.connectionInformation)]:[],...(null!==(o=e.bandwidths)&&void 0!==o?o:[]).map(M),...e.timing.flatMap(V),...e.encryptionKey?[q(e.encryptionKey)]:[],...(null!==(a=e.group)&&void 0!==a?a:[]).map((e=>`a=group:${[e.semantics,...e.mids].join(" ")}`)),...W(e.iceLite,"ice-lite"),...G(e),...B(e),...e.direction?[`a=${e.direction}`]:[],...W(e.endOfCandidates,"end-of-candidates"),...Z(e),...(null!==(d=e.media)&&void 0!==d?d:[]).flatMap(_)].join("\r\n")+"\r\n"}},J={md2:16,md5:16,"sha-1":20,"sha-224":28,"sha-256":32,"sha-384":48,"sha-512":64},Q=/^[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2})*$/,X={validate:(t,r={})=>{var i,s,n,o;let d;if("string"==typeof t)try{d=S.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=[],l=(e,t,r)=>c.push(void 0===r?{severity:"error",code:e,message:t}:{severity:"error",code:e,message:t,mediaIndex:r}),m=(e,t,r)=>c.push(void 0===r?{severity:"warning",code:e,message:t}:{severity:"warning",code:e,message:t,mediaIndex:r}),u=null!==(i=d.media)&&void 0!==i?i:[];0!==d.protocolVersion&&l("invalid-version",`v= must be 0, got ${d.protocolVersion}`),d.origin||l("missing-origin","o= is required"),void 0===d.sessionName?l("missing-session-name","s= is required"):""===d.sessionName&&m("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)||l("missing-timing","at least one t= line is required"),d.connectionInformation||u.forEach(((e,t)=>{e.connectionInformation||l("missing-connection","c= is required at session level or in every media description (RFC 8866 §5.7)",t)}));const f=new Set;u.forEach(((e,t)=>{void 0!==e.mid&&(f.has(e.mid)&&l("duplicate-mid",`a=mid:${e.mid} is used by more than one media description`,t),f.add(e.mid))}));for(const e of null!==(n=d.group)&&void 0!==n?n:[])for(const t of e.mids)f.has(t)||l("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)))&&u.forEach(((e,t)=>{void 0===e.mid&&l("bundle-missing-mid","every media description needs a=mid when BUNDLE is used (RFC 9143)",t)}));const v=void 0!==d.iceUfrag||void 0!==d.icePwd||d.iceLite||u.some((e=>{var t;return void 0!==e.iceUfrag||void 0!==e.icePwd||(null===(t=e.candidates)||void 0===t?void 0:t.length)})),h=(e,t,r)=>{void 0!==e&&(e.length<4||e.length>256)&&l("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)&&l("ice-pwd-length",`ice-pwd must be 22-256 characters, got ${t.length} (RFC 8839 §5.4)`,r)};h(d.iceUfrag,d.icePwd);const g=(e,t)=>{var r;for(const{hashFunction:i,fingerprint:s}of null!==(r=e.fingerprints)&&void 0!==r?r:[]){const e=p(J,i.toLowerCase());Q.test(s)?void 0!==e&&s.split(":").length!==e?l("invalid-fingerprint",`a=fingerprint:${i} has ${s.split(":").length} bytes, expected ${e} (RFC 8122 §5)`,t):s!==s.toUpperCase()&&m("fingerprint-not-uppercase",`a=fingerprint:${i} must use upper-case hex (RFC 8122 §5)`,t):l("invalid-fingerprint",`a=fingerprint:${i} must be colon-separated hex pairs (RFC 8122 §5)`,t)}};return g(d),u.forEach(((e,t)=>{var i,s,n,o,c,p,u,f,$,w,y,b,x,k,E,T;const j=0!==e.port;if(h(e.iceUfrag,e.icePwd,t),g(e,t),v&&j&&(void 0===(null!==(i=e.iceUfrag)&&void 0!==i?i:d.iceUfrag)&&l("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)&&l("missing-ice-pwd","a=ice-pwd is required (RFC 8839 §5.4)",t)),(e=>/(^|\/)(D?TLS)(\/|$)/.test(e.proto))(e)&&j){(null===(n=e.fingerprints)||void 0===n?void 0:n.length)||(null===(o=d.fingerprints)||void 0===o?void 0:o.length)||l("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?l("missing-setup",`a=setup is required for ${e.proto} (RFC 8842 §5.2, §5.3)`,t):"answer"===r.type&&"actpass"===i&&l("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!==(p=e.codecs)&&void 0!==p?p:[])r.payloadType>34&&void 0===r.encodingName&&l("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||m("unknown-payload-type",`a=${r.name}:${r.value} refers to a payload type that is not in the m= line, or repeats`,t)}const P=new Set;for(const r of null!==(f=e.extmap)&&void 0!==f?f:[])(r.id<1||15===r.id||r.id>255)&&l("extmap-invalid-id",`extmap id ${r.id} must be 1-14 or 16-255 (RFC 8285)`,t),P.has(r.id)&&l("extmap-duplicate-id",`extmap id ${r.id} is used more than once`,t),P.add(r.id);for(const r of["send","recv"])for(const i of(null!==(w=null===($=e.simulcast)||void 0===$?void 0:$[r])&&void 0!==w?w:[]).flat())(null===(y=e.rids)||void 0===y?void 0:y.some((e=>e.id===i.rid&&e.direction===r)))||l("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!==(b=e.rids)&&void 0!==b?b:[])for(const i of null!==(x=r.payloadTypes)&&void 0!==x?x:[])(null===(k=e.codecs)||void 0===k?void 0:k.some((e=>e.payloadType===i)))||l("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!==(E=e.ssrcGroups)&&void 0!==E?E:[])for(const i of r.ssrcs)(null===(T=e.ssrcs)||void 0===T?void 0:T.some((e=>e.id===i)))||m("ssrc-group-unknown-ssrc",`a=ssrc-group:${r.semantics} references ssrc ${i} without an a=ssrc line`,t)})),c}},Y="1.2.0";export{e as SdpParseError,S as sdpReader,X as sdpValidator,H as sdpWriter,Y as version};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irbisadm/simple-sdp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Parse, modify, write and validate SDP (RFC 8866) for WebRTC and SIP. Typed ICE, DTLS, BUNDLE, simulcast, codecs. Zero dependencies.",
|
|
6
6
|
"author": {
|
|
@@ -22,7 +22,13 @@
|
|
|
22
22
|
"types": "./types/index.d.ts",
|
|
23
23
|
"import": "./dist/index.mjs",
|
|
24
24
|
"default": "./dist/index.mjs"
|
|
25
|
-
}
|
|
25
|
+
},
|
|
26
|
+
"./wasm": {
|
|
27
|
+
"types": "./wasm/dist/index.d.ts",
|
|
28
|
+
"import": "./wasm/dist/index.mjs",
|
|
29
|
+
"default": "./wasm/dist/index.mjs"
|
|
30
|
+
},
|
|
31
|
+
"./wasm/simple-sdp.wasm": "./wasm/dist/simple-sdp.wasm"
|
|
26
32
|
},
|
|
27
33
|
"sideEffects": false,
|
|
28
34
|
"keywords": [
|
|
@@ -37,11 +43,16 @@
|
|
|
37
43
|
"typescript",
|
|
38
44
|
"parser",
|
|
39
45
|
"simulcast",
|
|
40
|
-
"ice"
|
|
46
|
+
"ice",
|
|
47
|
+
"wasm"
|
|
41
48
|
],
|
|
42
49
|
"files": [
|
|
43
50
|
"./dist/index.mjs",
|
|
44
51
|
"./types/index.d.ts",
|
|
52
|
+
"./wasm/dist/index.mjs",
|
|
53
|
+
"./wasm/dist/index.d.ts",
|
|
54
|
+
"./wasm/dist/simple-sdp.mjs",
|
|
55
|
+
"./wasm/dist/simple-sdp.wasm",
|
|
45
56
|
"./readme.md",
|
|
46
57
|
"./CHANGELOG.md"
|
|
47
58
|
],
|
|
@@ -51,6 +62,7 @@
|
|
|
51
62
|
"@rollup/plugin-typescript": "^11.0.0",
|
|
52
63
|
"@types/jest": "^29.5.0",
|
|
53
64
|
"@types/node": "^18.14.0",
|
|
65
|
+
"assemblyscript": "0.28.20",
|
|
54
66
|
"jest": "^29.5.0",
|
|
55
67
|
"rollup": "^3.17.1",
|
|
56
68
|
"rollup-plugin-dts": "^5.2.0",
|
|
@@ -64,6 +76,10 @@
|
|
|
64
76
|
"scripts": {
|
|
65
77
|
"build": "rollup -c",
|
|
66
78
|
"test": "jest",
|
|
67
|
-
"typecheck": "tsc --noEmit"
|
|
79
|
+
"typecheck": "tsc --noEmit",
|
|
80
|
+
"asbuild": "asc --config asconfig.json --target release && node scripts/sync-wasm.mjs",
|
|
81
|
+
"asbuild:debug": "asc --config asconfig.json --target debug",
|
|
82
|
+
"test:wasm": "jest -c jest.wasm.config.js && node test-wasm/npm-entry.mjs",
|
|
83
|
+
"prepack": "yarn asbuild && yarn build"
|
|
68
84
|
}
|
|
69
85
|
}
|
package/readme.md
CHANGED
|
@@ -186,6 +186,79 @@ Every fixture parses, round-trips through the writer and validates without issue
|
|
|
186
186
|
|
|
187
187
|
Safari is not covered by fixtures yet.
|
|
188
188
|
|
|
189
|
+
## WebAssembly, iOS and Android
|
|
190
|
+
|
|
191
|
+
[`assembly/`](assembly) is a port of the library to [AssemblyScript](https://www.assemblyscript.org/).
|
|
192
|
+
It compiles to a WebAssembly module (≈90 KB, ≈32 KB gzipped) with no imports, so the same
|
|
193
|
+
parser, writer and validator run in any wasm runtime: browsers and Node.js, iOS through a
|
|
194
|
+
wasm interpreter (no JIT, as the App Store requires), Android and the JVM as bytecode
|
|
195
|
+
compiled from the module at build time.
|
|
196
|
+
|
|
197
|
+
The port behaves exactly like the TypeScript code. `yarn test:wasm` runs the test suites of
|
|
198
|
+
this package against the wasm build, plus a differential test that compares both
|
|
199
|
+
implementations on all fixtures and on thousands of mutations of them: same model, same
|
|
200
|
+
written text, same issues, same error messages.
|
|
201
|
+
|
|
202
|
+
| Platform | Get it | Runtime |
|
|
203
|
+
|---|---|---|
|
|
204
|
+
| iOS 18+, macOS 15+ | SwiftPM: `.package(url: "https://github.com/irbisadm/simple-sdp.git", from: "1.2.0")`, product `SimpleSdp` ([`platforms/apple`](platforms/apple)) | [WasmKit](https://github.com/swiftwasm/WasmKit) interpreter |
|
|
205
|
+
| Android, JVM 11+ | JitPack: `maven("https://jitpack.io")`, `implementation("com.github.irbisadm:simple-sdp:v1.2.0")` ([`platforms/kotlin`](platforms/kotlin)) | [Chicory](https://chicory.dev), compiled to bytecode at build time |
|
|
206
|
+
| Browser, Node.js | npm: `import {sdpReader} from '@irbisadm/simple-sdp/wasm'` — the API of the main entry point | the JS engine |
|
|
207
|
+
| Other wasm hosts | `simple-sdp.wasm` from [GitHub Releases](https://github.com/irbisadm/simple-sdp/releases) or `@irbisadm/simple-sdp/wasm/simple-sdp.wasm` | any |
|
|
208
|
+
|
|
209
|
+
For JavaScript the TypeScript build is the faster choice (see the timings below); the
|
|
210
|
+
`/wasm` entry point exists for parity checks and for environments that prefer one shared
|
|
211
|
+
implementation. It is an ES module with top-level await that loads `simple-sdp.wasm` through
|
|
212
|
+
`new URL("simple-sdp.wasm", import.meta.url)`: Node.js 17+ and Vite handle it as is.
|
|
213
|
+
|
|
214
|
+
The model crosses the wasm boundary as JSON with the shape of the TypeScript `Sdp` interface.
|
|
215
|
+
|
|
216
|
+
```swift
|
|
217
|
+
let sdp = try SimpleSdp()
|
|
218
|
+
let json = try sdp.parseJSON(offer) // Data: the model, key order kept
|
|
219
|
+
let issues = try sdp.validate(json: json, type: .offer)
|
|
220
|
+
let text = try sdp.write(json: json)
|
|
221
|
+
let model = try sdp.parse(offer) // [String: Any] (does not keep key order)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
```kotlin
|
|
225
|
+
val sdp = SimpleSdp()
|
|
226
|
+
val model = sdp.parse(offer) // kotlinx.serialization JsonObject
|
|
227
|
+
val issues = sdp.validate(model, SdpType.OFFER)
|
|
228
|
+
val text = sdp.write(model)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Module interface
|
|
232
|
+
|
|
233
|
+
The module exports a C-style interface on UTF-8 buffers for hosts without AssemblyScript
|
|
234
|
+
bindings:
|
|
235
|
+
|
|
236
|
+
| Export | |
|
|
237
|
+
|---|---|
|
|
238
|
+
| `sdp_alloc(len) → ptr` | allocate an input buffer; write the UTF-8 bytes at `ptr` |
|
|
239
|
+
| `sdp_parse(ptr, len) → res` | `{"sdp": model}` or `{"error": {message, reason, lineNumber, line}}` |
|
|
240
|
+
| `sdp_write(ptr, len) → res` | model JSON in; `{"sdp": "text"}` or `{"error": {message}}` |
|
|
241
|
+
| `sdp_validate(ptr, len, type) → res` | SDP text in, `type` 0 = none, 1 = offer, 2 = answer; `{"issues": [...]}` |
|
|
242
|
+
| `sdp_validate_model(ptr, len, type) → res` | model JSON in; `{"issues": [...]}` or `{"error": {message}}` |
|
|
243
|
+
| `sdp_version() → res` | version string |
|
|
244
|
+
| `sdp_len(res) → len` | byte length of a buffer |
|
|
245
|
+
| `sdp_free(ptr)` | release an input buffer or a result |
|
|
246
|
+
|
|
247
|
+
A module instance is single-threaded; the Swift and Kotlin wrappers serialize calls.
|
|
248
|
+
The module uses the minimal AssemblyScript runtime: memory is reclaimed at the end of every
|
|
249
|
+
call, so a result stays valid until the next call on the same instance.
|
|
250
|
+
|
|
251
|
+
Approximate time to parse / write an 18 KB Chrome offer on x86-64, JSON transfer included:
|
|
252
|
+
wasm in Node.js 1.3 / 1.2 ms (the TypeScript build: 0.35 / 0.25 ms), JVM with the compiled
|
|
253
|
+
module 6 / 9 ms, WasmKit (release build) ≈10 ms each.
|
|
254
|
+
|
|
255
|
+
The generated ESM bindings (`wasm/build/simple-sdp.js`, published as
|
|
256
|
+
`wasm/dist/simple-sdp.mjs`) take strings: `parse(text)`, `write(json)`, `validate(text, type)`,
|
|
257
|
+
`validateModel(json, type)`.
|
|
258
|
+
|
|
259
|
+
Build: `yarn asbuild` compiles the module and copies it into `platforms/` (the copies are
|
|
260
|
+
committed so that SwiftPM and Gradle builds do not need Node.js).
|
|
261
|
+
|
|
189
262
|
## License
|
|
190
263
|
|
|
191
264
|
MIT
|
package/types/index.d.ts
CHANGED
|
@@ -332,6 +332,6 @@ declare class SdpParseError extends Error {
|
|
|
332
332
|
constructor(reason: string, lineNumber: number, line: string);
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
-
declare const version = "1.0
|
|
335
|
+
declare const version = "1.2.0";
|
|
336
336
|
|
|
337
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 };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// @irbisadm/simple-sdp/wasm — the API of the main entry point, run by the WebAssembly build
|
|
2
|
+
// (assembly/). Published as wasm/dist/index.mjs next to simple-sdp.mjs and simple-sdp.wasm.
|
|
3
|
+
import {parse as rawParse, write as rawWrite, validate as rawValidate, validateModel as rawValidateModel, VERSION}
|
|
4
|
+
from './simple-sdp.mjs';
|
|
5
|
+
import {SdpParseError} from '../../dist/index.mjs';
|
|
6
|
+
|
|
7
|
+
export {SdpParseError};
|
|
8
|
+
|
|
9
|
+
const parse = (sdp) => {
|
|
10
|
+
const result = JSON.parse(rawParse(sdp));
|
|
11
|
+
if (result.error) {
|
|
12
|
+
throw new SdpParseError(result.error.reason, result.error.lineNumber, result.error.line);
|
|
13
|
+
}
|
|
14
|
+
return result.sdp;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const isValid = (sdp, writeLog = false) => {
|
|
18
|
+
try {
|
|
19
|
+
parse(sdp);
|
|
20
|
+
return true;
|
|
21
|
+
} catch (e) {
|
|
22
|
+
if (writeLog) {
|
|
23
|
+
console.error(e);
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const write = (sdp) => {
|
|
30
|
+
const result = JSON.parse(rawWrite(JSON.stringify(sdp)));
|
|
31
|
+
if (result.error) {
|
|
32
|
+
throw new Error(result.error.message);
|
|
33
|
+
}
|
|
34
|
+
return result.sdp;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const validate = (input, options = {}) => {
|
|
38
|
+
const type = options.type ?? null;
|
|
39
|
+
const result = JSON.parse(typeof input === 'string'
|
|
40
|
+
? rawValidate(input, type)
|
|
41
|
+
: rawValidateModel(JSON.stringify(input), type));
|
|
42
|
+
if (result.error) {
|
|
43
|
+
throw new Error(result.error.message);
|
|
44
|
+
}
|
|
45
|
+
return result.issues;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const sdpReader = {parse, isValid};
|
|
49
|
+
export const sdpWriter = {write};
|
|
50
|
+
export const sdpValidator = {validate};
|
|
51
|
+
export const version = VERSION.value;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
async function instantiate(module, imports = {}) {
|
|
2
|
+
const { exports } = await WebAssembly.instantiate(module, imports);
|
|
3
|
+
const memory = exports.memory || imports.env.memory;
|
|
4
|
+
const adaptedExports = Object.setPrototypeOf({
|
|
5
|
+
VERSION: {
|
|
6
|
+
// assembly/index/VERSION: ~lib/string/String
|
|
7
|
+
valueOf() { return this.value; },
|
|
8
|
+
get value() {
|
|
9
|
+
return __liftString(exports.VERSION.value >>> 0);
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
isValid(sdp) {
|
|
13
|
+
// assembly/index/isValid(~lib/string/String) => bool
|
|
14
|
+
sdp = __lowerString(sdp) || __notnull();
|
|
15
|
+
return exports.isValid(sdp) != 0;
|
|
16
|
+
},
|
|
17
|
+
parse(sdp) {
|
|
18
|
+
// assembly/index/parse(~lib/string/String) => ~lib/string/String
|
|
19
|
+
sdp = __lowerString(sdp) || __notnull();
|
|
20
|
+
return __liftString(exports.parse(sdp) >>> 0);
|
|
21
|
+
},
|
|
22
|
+
write(model) {
|
|
23
|
+
// assembly/index/write(~lib/string/String) => ~lib/string/String
|
|
24
|
+
model = __lowerString(model) || __notnull();
|
|
25
|
+
return __liftString(exports.write(model) >>> 0);
|
|
26
|
+
},
|
|
27
|
+
validate(sdp, type) {
|
|
28
|
+
// assembly/index/validate(~lib/string/String, ~lib/string/String | null?) => ~lib/string/String
|
|
29
|
+
sdp = __retain(__lowerString(sdp) || __notnull());
|
|
30
|
+
type = __lowerString(type);
|
|
31
|
+
try {
|
|
32
|
+
exports.__setArgumentsLength(arguments.length);
|
|
33
|
+
return __liftString(exports.validate(sdp, type) >>> 0);
|
|
34
|
+
} finally {
|
|
35
|
+
__release(sdp);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
validateModel(model, type) {
|
|
39
|
+
// assembly/index/validateModel(~lib/string/String, ~lib/string/String | null?) => ~lib/string/String
|
|
40
|
+
model = __retain(__lowerString(model) || __notnull());
|
|
41
|
+
type = __lowerString(type);
|
|
42
|
+
try {
|
|
43
|
+
exports.__setArgumentsLength(arguments.length);
|
|
44
|
+
return __liftString(exports.validateModel(model, type) >>> 0);
|
|
45
|
+
} finally {
|
|
46
|
+
__release(model);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
sdp_alloc(len) {
|
|
50
|
+
// assembly/index/sdp_alloc(i32) => usize
|
|
51
|
+
return exports.sdp_alloc(len) >>> 0;
|
|
52
|
+
},
|
|
53
|
+
sdp_parse(ptr, len) {
|
|
54
|
+
// assembly/index/sdp_parse(usize, i32) => usize
|
|
55
|
+
return exports.sdp_parse(ptr, len) >>> 0;
|
|
56
|
+
},
|
|
57
|
+
sdp_write(ptr, len) {
|
|
58
|
+
// assembly/index/sdp_write(usize, i32) => usize
|
|
59
|
+
return exports.sdp_write(ptr, len) >>> 0;
|
|
60
|
+
},
|
|
61
|
+
sdp_validate(ptr, len, type) {
|
|
62
|
+
// assembly/index/sdp_validate(usize, i32, i32) => usize
|
|
63
|
+
return exports.sdp_validate(ptr, len, type) >>> 0;
|
|
64
|
+
},
|
|
65
|
+
sdp_validate_model(ptr, len, type) {
|
|
66
|
+
// assembly/index/sdp_validate_model(usize, i32, i32) => usize
|
|
67
|
+
return exports.sdp_validate_model(ptr, len, type) >>> 0;
|
|
68
|
+
},
|
|
69
|
+
sdp_version() {
|
|
70
|
+
// assembly/index/sdp_version() => usize
|
|
71
|
+
return exports.sdp_version() >>> 0;
|
|
72
|
+
},
|
|
73
|
+
}, exports);
|
|
74
|
+
function __liftString(pointer) {
|
|
75
|
+
if (!pointer) return null;
|
|
76
|
+
const
|
|
77
|
+
end = pointer + new Uint32Array(memory.buffer)[pointer - 4 >>> 2] >>> 1,
|
|
78
|
+
memoryU16 = new Uint16Array(memory.buffer);
|
|
79
|
+
let
|
|
80
|
+
start = pointer >>> 1,
|
|
81
|
+
string = "";
|
|
82
|
+
while (end - start > 1024) string += String.fromCharCode(...memoryU16.subarray(start, start += 1024));
|
|
83
|
+
return string + String.fromCharCode(...memoryU16.subarray(start, end));
|
|
84
|
+
}
|
|
85
|
+
function __lowerString(value) {
|
|
86
|
+
if (value == null) return 0;
|
|
87
|
+
const
|
|
88
|
+
length = value.length,
|
|
89
|
+
pointer = exports.__new(length << 1, 2) >>> 0,
|
|
90
|
+
memoryU16 = new Uint16Array(memory.buffer);
|
|
91
|
+
for (let i = 0; i < length; ++i) memoryU16[(pointer >>> 1) + i] = value.charCodeAt(i);
|
|
92
|
+
return pointer;
|
|
93
|
+
}
|
|
94
|
+
const refcounts = new Map();
|
|
95
|
+
function __retain(pointer) {
|
|
96
|
+
if (pointer) {
|
|
97
|
+
const refcount = refcounts.get(pointer);
|
|
98
|
+
if (refcount) refcounts.set(pointer, refcount + 1);
|
|
99
|
+
else refcounts.set(exports.__pin(pointer), 1);
|
|
100
|
+
}
|
|
101
|
+
return pointer;
|
|
102
|
+
}
|
|
103
|
+
function __release(pointer) {
|
|
104
|
+
if (pointer) {
|
|
105
|
+
const refcount = refcounts.get(pointer);
|
|
106
|
+
if (refcount === 1) exports.__unpin(pointer), refcounts.delete(pointer);
|
|
107
|
+
else if (refcount) refcounts.set(pointer, refcount - 1);
|
|
108
|
+
else throw Error(`invalid refcount '${refcount}' for reference '${pointer}'`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function __notnull() {
|
|
112
|
+
throw TypeError("value must not be null");
|
|
113
|
+
}
|
|
114
|
+
return adaptedExports;
|
|
115
|
+
}
|
|
116
|
+
export const {
|
|
117
|
+
memory,
|
|
118
|
+
__new,
|
|
119
|
+
__pin,
|
|
120
|
+
__unpin,
|
|
121
|
+
__collect,
|
|
122
|
+
__rtti_base,
|
|
123
|
+
VERSION,
|
|
124
|
+
isValid,
|
|
125
|
+
parse,
|
|
126
|
+
write,
|
|
127
|
+
validate,
|
|
128
|
+
validateModel,
|
|
129
|
+
sdp_alloc,
|
|
130
|
+
sdp_free,
|
|
131
|
+
sdp_len,
|
|
132
|
+
sdp_parse,
|
|
133
|
+
sdp_write,
|
|
134
|
+
sdp_validate,
|
|
135
|
+
sdp_validate_model,
|
|
136
|
+
sdp_version,
|
|
137
|
+
} = await (async url => instantiate(
|
|
138
|
+
await (async () => {
|
|
139
|
+
const isNodeOrBun = typeof process != "undefined" && process.versions != null && (process.versions.node != null || process.versions.bun != null);
|
|
140
|
+
if (isNodeOrBun) { return globalThis.WebAssembly.compile(await (await import("node:fs/promises")).readFile(url)); }
|
|
141
|
+
else { return await globalThis.WebAssembly.compileStreaming(globalThis.fetch(url)); }
|
|
142
|
+
})(), {
|
|
143
|
+
}
|
|
144
|
+
))(new URL("simple-sdp.wasm", import.meta.url));
|
|
Binary file
|