{"id":18147439,"url":"https://github.com/thestaticturtle/aodns","last_synced_at":"2025-06-13T12:32:43.231Z","repository":{"id":202825453,"uuid":"707928472","full_name":"TheStaticTurtle/AoDNS","owner":"TheStaticTurtle","description":"AoDNS, or Audio over DNS is a method of streaming audio data with TXT queries. ","archived":false,"fork":false,"pushed_at":"2023-10-21T20:36:06.000Z","size":36,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-06-11T04:54:59.910Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TheStaticTurtle.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2023-10-21T02:21:47.000Z","updated_at":"2023-12-05T17:15:37.000Z","dependencies_parsed_at":null,"dependency_job_id":"266db070-fd07-4f44-8e30-6e29f59b2e23","html_url":"https://github.com/TheStaticTurtle/AoDNS","commit_stats":null,"previous_names":["thestaticturtle/aodns"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/TheStaticTurtle/AoDNS","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheStaticTurtle%2FAoDNS","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheStaticTurtle%2FAoDNS/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheStaticTurtle%2FAoDNS/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheStaticTurtle%2FAoDNS/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TheStaticTurtle","download_url":"https://codeload.github.com/TheStaticTurtle/AoDNS/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheStaticTurtle%2FAoDNS/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259645095,"owners_count":22889528,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-01T22:06:33.152Z","updated_at":"2025-06-13T12:32:43.171Z","avatar_url":"https://github.com/TheStaticTurtle.png","language":"Python","readme":"# AoDNS\n\nAoDNS, or Audio over DNS is a method of streaming data with TXT queries. \nThe underlying concept is pretty simple basically there is a subdomain that gives the list of sequence number available and multiple sequence subdomains that holds the data.\nThis explained in more details below\n\nDue to the module approach, this AoDNS implementation is easily extendable. \nYou could for example add a new codec, add encryption, .... or anything else really, very easily.\n\nLatency varies a lot depending on the configuration. \nIf the client directly connects to the server, you can get away by using a small number of sequence same if proxied directly \n\nHowever, if you have a proxy that respect ttl (which is most of them), you'll need to increase the number of sequences that are available.\nthe TTL of the ``sequence`` endpoint is fixed to 10sec, and the TLL of the sequences domains is automatically calculated to be around 80% of the total time of audio data saved in all sequences\n\n## Usage\n\nThis is for local settings only.\n\nFirst run the server:\n```\n# python -m aodns.server music.example.com --dns-port 5053\n```\nThe default is 75 sequence number, so you might run into some underflow if you start the client right away, I suggest wait 10-20 seconds.\n\nThen run the client:\n```\n# python -m aodns.client --dns-port 5053 music.example.com 127.0.0.1\n```\n\n\n## How does it work\n### Server\n#### Capture\nAudio capture is done by default with pyaudio but can be extended to any source that supports arbitrary raw pcm frame lengths.\n\nBecause most codecs require a specific frame size, it is queried during setup and can not be changed\n\nSample rate and Channel count are also fixed but can be adjusted. Using the opus codec I was able to get 16kHz 1ch working.\n\n#### Compression (codec)\n\nRight now, two codec are implemented but only one officially tested:\n- [Opus](https://opus-codec.org/) (tested, working)\n- [Codec2](https://rowetel.com/codec2.html) **(untested)**\n\nOpus provides a better audio quality than Codec2 but Codec2 will produce fewer data.\n\nBy default, AoDNS will use opus.\n\n#### Encoding (asciializing)\n\nBecause dns TXT records and because of compatibility, the compressed audio frames needs to be converted to ascii chars.\nBase64 will be the immediate choice of many but not the most space efficient one:\n```repl\n\u003e\u003e\u003e t = \"abcd\"*64\n\u003e\u003e\u003e len(t)\n256\n\u003e\u003e\u003e len(base64.b16encode(t.encode(\"utf-8\")))\n512\n\u003e\u003e\u003e len(base64.b32encode(t.encode(\"utf-8\")))\n416\n\u003e\u003e\u003e len(base64.b64encode(t.encode(\"utf-8\")))\n344\n\u003e\u003e\u003e len(base91.encode(t.encode(\"utf-8\")))\n315\n```\n\nHence, why, by default base91 is used to encode the data\n\n#### Data ordering and packing\n\nTXT records only supports 255 bytes per string, however, a domain can have multiple TXT strings.\n\nAfter some testing, it seems that 3k is approximately the maximum supported by the Windows server 2020 responder. \nApparently some even support up to 4k.\n\nTo be on the safe side, by default one sequence domain will only store a maximum of 2kB\n\nIf you are using the client to connect directly to the server there would be an issue.\nBut AoDNS is able to work behind dns resolvers and unfortunately, some resolver will return the TXT strings in a random order:\n\n```\n# dig +ttlunit TXT seq_49680.music.example.com\n\n;; ANSWER SECTION:\nseq_49680.music.example.com. 21s IN TXT  \"003'''8U|7E/J`Cc82|8XLpLk^T__5uf~F8PN7~Mfa(E^\u003eWsjNBHV(\u003c2AF|H\u003cus_*}Dyt68)v%:^`\u003cw?\u0026OxM}_ZUf6S\u0026KTbCtFQp$sD!]*a\\\"\"\nseq_49680.music.example.com. 21s IN TXT  \"001'''8U{OOU8]UVh.PiU!N=6WJ3k@\u003e+fl\u0026\u0026\u003c`\u003eSnpY4Tzo_%W(Z_rs)@jzY1x|abYVng=d^vUg5cY\\\"+K~@OsihU3cxIyz+\u0026MMQu!m,\u003e=dHbsaD\"\nseq_49680.music.example.com. 21s IN TXT  \"004'''rXSKF[23x}`Z!lP@HP8lDSWPhw^ViX!^CZHUgV9m=p}~kb5yk89@f*4/%Kk_!c2[{|\\\"d,~?B:u|RltK]Ce]t0zf5S{h6H;W\"\nseq_49680.music.example.com. 21s IN TXT  \"005'''8Uy]!Pn!e%1J\u0026xz;V,~d@7J%Ca#ct]D7B^U?u4|],2IE]7ZuE$9;2n;V~{UmwB8P;)=cU/LSdxGvNn\u003e{PoBFUHx:\u00264]jrS^aDu{ve7)\\\"EU\"\nseq_49680.music.example.com. 21s IN TXT  \"006'''8Uy]jZ/.]MJLfG%lrc[5FJuJ8$Ixk9VfRjLsLg*Hc`P!x1sHSZpIv5%Rf%|DfXm]%Lr}B%$lRn1oYmOZ+C|sM9bZi6\u0026H+$NipeyS\u003c\u003e|bVvT\"\nseq_49680.music.example.com. 21s IN TXT  \"007'''8Uq\u0026l\u003c\\\"!uR/i})@E]\u003cTsXZ%0\u0026ulQ%YB|p97tmV)mA|8[+M3;~G`XSu*cKo^Yg`Mq:JAiR{W*x`9mHe\u003ee\u0026?LfsL1!boJ^\u0026\\\"[PPER0.(\"\nseq_49680.music.example.com. 21s IN TXT  \"008'''rX%=qo9rl2P@`BC!\u003eT,O\u003e(+ZU?hp}]WQT^MGki99SL18Ho[!yUL4]6jgEM*5eqkhMLs}fIwx\u00265N*^\u0026XJv#0zG:5@vtC8|\u003e[cO(.U_^WeFNFB\"\nseq_49680.music.example.com. 21s IN TXT  \"009'''rX%=Ai]%`3:7z5swE1HMB9BS+7Wv@7\\\"w+nYNl\u0026\\\"M_Z*6=T\u003cp\u003eZp?Kwo|M},B;$Uwf`c~UrkLbUDuk8H69%},V}X\u003eJa~QRD\"\nseq_49680.music.example.com. 21s IN TXT  \"010'''rXKK!74bs7GWw.mBS/@NVj7gCgGy_@sv*9+]tTvB;6a|*N*6Lr.fDcyb\u0026sH2(f=_#zV?^Tb@H)De]\u003cj^.4?3])hA\"\nseq_49680.music.example.com. 21s IN TXT  \"011'''rXSK]\\\"hAk4R.zCnc{\\\";RBm[~,Kgl`kB]sxx^b=9PW\u0026n.sK~1MWbOd[E:IdL`6Y%*3@\u003e?`M2Sh^cmAH~3]3;[uJl`\u003e5ITZUK5\\\"m!C\"\nseq_49680.music.example.com. 21s IN TXT  \"012'''rX%=M5c+YU5JS`di#g7xy=;n~TrBLt*5FFhWpQ\\\"S#li//^[1xA*Wgsd]l+K~F1znI;\u003cy30[Y2!A\\\"OlDY:d[)8_DAC\"\nseq_49680.music.example.com. 21s IN TXT  \"013'''rXn;Erky{T\u003e_HKk\u003cOV2UD?wyYfEWMgbsJ?/HV;nMW,]`\u003cO9Z*Wp[}\u003cZ0n0.J4(KmD:(by2/187?oOsuL3_3}bUE2`xNjX[E1h4v%r\\\"\"\nseq_49680.music.example.com. 21s IN TXT  \"014'''8U{O%;Nl\u003eEKbAo.w2?@(HJzsf(Ip9|Ac`rg1\u003ehS!7tS(Uw68%gO#8M(Vf7\u003eEZ.yN;zElg[Ga7(?_lMw%0y#d:oAa\u0026)|6T3A\u003elB\"\nseq_49680.music.example.com. 21s IN TXT  \"015'''}AP;v[(v.gHQHsjlM/I{`60)WHD9^ia*\u003cLgq\u00265.iY9lM8$::#;zCp)06W\u003clE\u003exi9.T;]hd3$!aKaCmhG8G7wR#D[\u003cnZZJ\u003cWP$\"\nseq_49680.music.example.com. 21s IN TXT  \"016'''rX%=Nm;nn6uRG\u003c,JM:A5/sQJ~\u003eqB/\u003co:iu^\\\"sX]Z`n)oHv9lzqP%VEM5OsBW8LEP1mK\\\"CLAcDrK.|8%s)~)V%h33Kr,8$ba|KvB\"\nseq_49680.music.example.com. 21s IN TXT  \"017'''rX$gRf8^ooNsOpLYL5Zy@(5%,j^*qG3K|L}/]G\u0026#^1#;o]mRRiH13zyw_^jZ#w2%s`6Qg.wQDR\u003ch?MAQ6!`43ov]+Ez\u002675b\u003c?k0BH34QE\"\nseq_49680.music.example.com. 21s IN TXT  \"018'''8U+O@Nv/(\u0026nR|5\u0026kW$m6}laH`p\u003cZp|LCx{t_lc1;!U6:.k!\u003e.7I8`\u003cy?*a#?(SvK@gRwV\u0026WYs@gdzNOHc4\u003ccst4\u003ecBHw[/oG*E9@FZ=CN\"\nseq_49680.music.example.com. 21s IN TXT  \"000'''8U{O#xvTKUK{a)`$*k0t`OwPP$B+(Ogky5*um7:PR2$UN1Q.{J!NZGWJ1~n=].^osw7\u0026qGSY=sw)]WptNCJ;fO4e}d4ybQIXTTt]on*E\"\nseq_49680.music.example.com. 21s IN TXT  \"002'''8U4}l=%kj@6Ph3j9aZJO*qQ,2R*m2z[Kp.;h:%m6);/CR)8J4l}Im9IU5)H+KGLT4T`,(Jt}JwBmEqxMw7[pCUYPO,8\u003eT^9jl.[:w6)EG\"\n```\n\nThis is why all strings are prefixed with their index. The index and data is separated by 3 `'` which is a char not used in base91 \n\n#### Sequence generation \u0026 rotation\n\nThe sequence generator is basically a way to index and store the packed data.\nThe number of stored packed data, directly affects the delay that the client will have.\n\nOn the other hand, having a low number of stored data will cause issues with the TTL of the records, especially to the `sequence` domain which is fixed to 10sec.\n\n#### Conversion to DNS records\n\nThe records generator is a simple class that takes the sequence generator and frame size.\n\nIt will output a list of DNS records that the DNS server can then serve.\nThere will be `number_of_stored_packed_data + 1` records\n\nThe `sequence.music.example.com` is a TXT record with one string that holds all the available sequence numbers separated by `,`\n\nDue to the max length of string being 255, the string is compressed with zlib and then base91 encoded.\n\nEven with this protection there is a possibility that if the server is left running for long and the number of stored packed data is high that the server will crash because the data will be longer tha 255 chars. \n\nAs the sequence numbers are linear on the server side, this could potentially be improved by sending the first sequence number and number of records.\n\n##### TTL\n\nTTL is a big issue, especially if the server is behind resolvers which might not respect low ttls.\n\nThe ttl of the `sequence.music.example.com` record is fixed to 10 seconds to ensure that it will be somewhat up to date before the client run out of sequences.\n\nThe ttl of sequence subdomains is dynamically calculated to be around 80% of the total length of audio data stored in all the records. Forumal looks like this: \n```python\nseq_duration = (codec_frame_size * pack.frame_count) / (AUDIO_SAMPLE_RATE * AUDIO_CHANNELS * 2)\nttl = int(seq_duration * sequencer.max_concurrent_numbers * 0.8)\n```\n\nWhich for the default settings and 75 sequence domains is around 23sec\n\n\n### Client\n#### DNS Resolution\nA custom dns resolver is used to run TXT lookup on domains. \nThe resolver can connect to any DNS server (with any port) but it must respond in \u003c 100ms if not, the client will retry up to 10 times at 250ms interval\n\n##### Reading the sequence list\nGetting the sequence list is done by resolving the `sequence` domain, decoding the base91 and de-compressing the zlib data.\nOnce done its convert back to list of ints\n\nThe `get_available_sequences` function has a timed cache set to 4 seconds to avoid spamming the DNS server (TTL is 10sec anyway)\nThe function also bypasses the retry counter and will run indefinitely until it gets an answer\n\n##### Reading sequences data\n\nReading a sequence is done by:\n - Getting all the strings in the TXT field\n - Preparing a `PackedData` instance\n - Using the `PackedData.insert_indexed` function to insert the base91 frame at the specified index\n\nNo additional checks are in place to check the continuity of the frame (eg detecting that 3 is missing in 1, 2, 4)\n\n#### Sequence reconstruction\n\nThe sequence reconstructor takes PackedData and it's sequence number and stores it in an internal buffer.\nOnce read, the sequence data is set to `None` indicating that it has been read and can be cleaned\n\nCleaning the sequence reconstructions remove all sequences which have no data.\n\nA special check is in place to make sure not to delete sequences that are still available on the resolver.\nThis avoids re-querying sequences that have already been read which would cause skipping\n\nIf by chance a sequence number lower than the one last but one that is not present in the internal buffer is received, it gets added via the `add_dummy` function which add the sequence number to the buffer without any data attached.\n\n#### Sequence reading\n\nThe `SequenceReconstructor` implements an iterator which will get the lowest sequence number possible in the buffer, mark it as read and return it.\nIf the internal buffer is empty, it will block until one is available\n\nThe `SequenceReader` takes  the instance of the reconstructor, alongside the asciializer, the codec and the streamer.\n\nThen it iterates over the reconstruct, de-asciialize it, de-compress it and streams it to the output device\n\nNote that the client doesn't know about the configuration of the server. Something that could easily be improved by adding a config record.\nThis does mean that if the settings are wrong, it might lead to corrupted data on the output.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthestaticturtle%2Faodns","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthestaticturtle%2Faodns","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthestaticturtle%2Faodns/lists"}