Skip to content

Instantly share code, notes, and snippets.

@LoggeL
Last active October 5, 2026 12:34
Show Gist options
  • Select an option

  • Save LoggeL/8a43b63341bf453b70d450ccc727110e to your computer and use it in GitHub Desktop.

Select an option

Save LoggeL/8a43b63341bf453b70d450ccc727110e to your computer and use it in GitHub Desktop.
Teams Video Downloader
// Paste into DevTools Console on the open SharePoint/Stream recording page.
(async()=>{"use strict";const e=(e,t,n)=>{if(!Number.isFinite(n)||n<=0)throw new Error("Invalid recording duration");const r=e=>{throw new Error(`MP4: ${e}`)};const i=e=>new DataView(e.buffer,e.byteOffset,e.byteLength);const o=(e,t)=>i(e).getUint32(t);const s=(e,t)=>String.fromCharCode(...e.subarray(t,t+4));const a=(e,t=0,n=e.length)=>{const a=[];for(let d=t;d<n;){if(n-d<8)r("truncated box header");let t=o(e,d),c=8;if(t===1){if(n-d<16)r("truncated extended box header");const o=i(e).getBigUint64(d+8);if(o>BigInt(Number.MAX_SAFE_INTEGER))r("box is too large");t=Number(o);c=16}else if(t===0)t=n-d;if(t<c||t>n-d)r("invalid box size");a.push({type:s(e,d+4),start:d,end:d+t,header:c,body:d+c});d+=t}return a};const d=(e,t)=>a(e,t.body,t.end);const c=(e,t)=>{const n=e.filter(e=>e.type===t);if(n.length!==1)r(`expected one ${t} box`);return n[0]};const l=(e,t)=>{if(e.end-e.body<t)r(`truncated ${e.type}`)};const u=(e,t,n)=>{if(!Number.isSafeInteger(n)||n<0||n>4294967295)r("32-bit duration overflow");i(e).setUint32(t,n)};const f=(e,t,n,o)=>{if(!Number.isSafeInteger(n)||n<0)r("duration overflow");if(o)i(e).setBigUint64(t,BigInt(n));else u(e,t,n)};const m=(e,t)=>{l(t,4);const n=e[t.body];if(n!==0&&n!==1)r(`unsupported ${t.type} version`);const i=n===1;l(t,i?32:20);const s=o(e,t.body+(i?20:12));if(!s)r(`zero timescale in ${t.type}`);return{scale:s,position:t.body+(i?24:16),wide:i}};const b=e=>{const t=new Uint8Array(e.reduce((e,t)=>e+t.length,0));let n=0;for(const r of e){t.set(r,n);n+=r.length}return t};const h=(e,t)=>{const n=b(t),r=new Uint8Array(8+n.length);u(r,0,r.length);for(let t=0;t<4;t++)r[4+t]=e.charCodeAt(t);r.set(n,8);return r};const g=(e,t)=>e.subarray(t.start,t.end);const p=(e,t,i)=>{if(!(e instanceof Uint8Array))r("initialization must be Uint8Array");const b=e.slice(),h=a(b),g=c(h,"moov");const p=d(b,g),w=c(p,"mvhd"),y=c(p,"trak");const v=d(b,y),A=c(v,"tkhd");l(A,4);const x=b[A.body]===1;if(b[A.body]!==0&&!x)r("unsupported tkhd version");l(A,x?36:24);const E=A.body+(x?20:12),S=o(b,E);if(!S)r("zero track id");u(b,E,i);const k=c(v,"mdia"),$=d(b,k),N=c($,"mdhd");const P=c($,"hdlr");l(P,12);if(s(b,P.body+8)!==(t==="video"?"vide":"soun"))r(`incorrect ${t} handler`);const D=c(d(b,c($,"minf")),"stbl"),M=c(d(b,D),"stsd");l(M,8);if(o(b,M.body+4)!==1)r("expected one sample description");const B=c(a(b,M.body+8,M.end),t==="video"?"avc1":"mp4a");const U=t==="video"?78:28;l(B,U);const T=a(b,B.body+U,B.end);if(T.some(e=>e.type==="sinf"))r("protected samples are unsupported");if(t==="video")c(T,"avcC");else c(T,"esds");const z=c(p,"mvex"),C=c(d(b,z),"trex");l(C,24);if(o(b,C.body+4)!==S)r("trex track mismatch");u(b,C.body+4,i);if(o(b,C.body+8)!==1)r("unsupported default sample description");for(const e of["stts","stsc","stco","co64"]){const t=d(b,D).find(t=>t.type===e);if(t){l(t,8);if(o(b,t.body+4))r("initialization contains nonfragmented samples")}}const I=m(b,w),L=m(b,N);f(b,L.position,Math.round(n*L.scale),L.wide);return{b,top:h,members:p,mvhd:w,trak:y,tkhd:A,tkhdWide:x,trex:C,originalId:S,newId:i,movieTime:I,track:v}};const w=p(e,"video",1),y=p(t,"audio",2);const v=w.movieTime.scale,A=Math.round(n*v);f(w.b,w.movieTime.position,A,w.movieTime.wide);l(w.mvhd,w.movieTime.wide?112:100);u(w.b,w.mvhd.end-4,3);for(const e of[w,y]){f(e.b,e.tkhd.body+(e.tkhdWide?28:20),A,e.tkhdWide);const t=e.track.find(e=>e.type==="edts");if(t){const n=c(d(e.b,t),"elst");l(n,8);const s=e.b[n.body]===1;if(e.b[n.body]!==0&&!s)r("unsupported edit list version");const a=o(e.b,n.body+4),u=s?20:12;if(a<1||a>2||n.end-n.body!==8+a*u)r("unsupported edit list");let m=0;for(let t=0;t<a;t++){const d=n.body+8+t*u,c=s?Number(i(e.b).getBigUint64(d)):o(e.b,d);const l=s?i(e.b).getBigInt64(d+8):BigInt(i(e.b).getInt32(d+4));const b=d+(s?16:8);if(o(e.b,b)!==65536)r("unsupported edit rate");if(t<a-1){if(l!==-1n)r("unsupported multiple media edits");m=Math.round(c*v/e.movieTime.scale);f(e.b,d,m,s)}else{if(l<0n||m>A)r("invalid media edit");f(e.b,d,A-m,s)}}}}const x=w.members.filter(e=>!["trak","mvex"].includes(e.type)).map(e=>g(w.b,e));const E=b([g(w.b,c(w.top,"ftyp")),h("moov",[...x,g(w.b,w.trak),g(y.b,y.trak),h("mvex",[g(w.b,w.trex),g(y.b,y.trex)])])]);let S=0;const k=(e,t)=>{if(!(e instanceof Uint8Array)||!["video","audio"].includes(t))r("invalid fragment input");const n=e.slice(),s=a(n),f=t==="video"?w:y;const m=[];for(let e=0;e<s.length;e++){const t=s[e];if(["sidx","mfra","styp","free","skip"].includes(t.type))continue;if(t.type!=="moof")r(`unexpected segment box ${t.type}`);const a=s[e+1];if(!a||a.type!=="mdat")r("moof must be followed by mdat");const b=d(n,t),h=c(b,"mfhd");l(h,8);u(n,h.body+4,++S);const p=c(b,"traf"),w=d(n,p),y=c(w,"tfhd");l(y,8);const v=o(n,y.body)&16777215;if(n[y.body]!==0||v&1)r("absolute base_data_offset is unsupported");if(v&~196667)r("unsupported tfhd flags");if(v&65536)r("empty-duration fragment is unsupported");const A=8+[2,8,16,32].filter(e=>v&e).length*4;if(y.end-y.body!==A)r("invalid tfhd length");if(o(n,y.body+4)!==f.originalId)r("fragment track mismatch");if(w.some(e=>["senc","saiz","saio"].includes(e.type)))r("protected fragment is unsupported");if(v&2){l(y,12);if(o(n,y.body+8)!==1)r("unknown sample description")}const x=c(w,"tfdt");l(x,8);if(n[x.body]>1||x.end-x.body!==(n[x.body]===1?12:8))r("invalid tfdt");u(n,y.body+4,f.newId);const E=w.filter(e=>e.type==="trun");if(!E.length)r("missing trun");for(const e of E){l(e,8);const s=o(n,e.body)&16777215,d=o(n,e.body+4);if(n[e.body]>1||s&~3845)r("unsupported trun fields");if(e===E[0]&&!(s&1))r("first trun needs a relative data offset");let c=8+(s&1?4:0)+(s&4?4:0);const u=[256,512,1024,2048].filter(e=>s&e).length*4;if(c+d*u!==e.end-e.body)r("invalid trun length");if(s&1){const o=t.start+i(n).getInt32(e.body+8);if(o<a.body||o>=a.end)r("trun offset is outside mdat")}}m.push(g(n,t),g(n,a));e++}if(!m.length)r("segment contains no fragments");return b(m)};return{init:E,fragment:k}};const t="[SharePoint video]";const n="sharepoint-video-download-panel";const r=window.__sharePointVideoDownload;if(["downloading","selecting"].includes(r?.status)){console.warn(`${t} Ein Download läuft bereits. Erst abbrechen oder fertigstellen lassen.`);return}document.getElementById(n)?.remove();const i={status:"preparing",completed:0,total:0,bytes:0,message:"Videostream wird geprüft ..."};window.__sharePointVideoDownload=i;let o=new AbortController;const s=(e,t)=>[...e.children].find(e=>e.localName===t);const a=(e,t)=>[...e.children].filter(e=>e.localName===t);const d=(e,t)=>[...e.getElementsByTagName("*")].filter(e=>e.localName===t);const c=e=>{const t=String(e||"").match(/^PT(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?$/);if(!t)throw new Error("Unbekannte Zeitangabe im DASH-Manifest.");return Number(t[1]||0)*3600+Number(t[2]||0)*60+Number(t[3]||0)};const l=e=>Uint8Array.from(atob(e),e=>e.charCodeAt(0));const u=(window.g_fileInfo?.name||document.title||"Aufnahme.mp4").replace(/[\\/:*?"<>|\x00-\x1f]/g,"-").replace(/\.mp4.*$/i,"").trim().slice(0,150)+".mp4";i.name=u;const f=document.createElement("section");f.id=n;f.style.cssText="position:fixed;bottom:20px;right:20px;z-index:2147483647;width:370px;max-width:85vw;padding:18px;background:white;color:#222;border:1px solid #888;border-radius:8px;box-shadow:0 4px 24px #0003;font:14px/1.5 system-ui";const m=document.createElement("strong");m.textContent="Aufzeichnung als MP4 speichern";const b=document.createElement("p");const h=document.createElement("progress");h.style.width="100%";const g=document.createElement("div");const p=e=>{const t=document.createElement("button");t.textContent=e;t.style.cssText="margin:8px 8px 0 0;padding:8px 10px;cursor:pointer";g.appendChild(t);return t};const w=p("MP4 speichern");const y=p("Über Brave herunterladen");const v=p("Schließen");w.disabled=y.disabled=true;f.append(m,b,h,g);document.body.appendChild(f);let A=false;const x=e=>{if(e)i.message=e;b.textContent=i.message;h.max=i.total||1;h.value=i.completed};v.addEventListener("click",()=>{if(i.status==="downloading"){o.abort();x("Download wird abgebrochen ...")}else{A=true;o.abort();f.remove()}});x();const E=async(e,t=false)=>{const n=new URL(e,location.href);if(n.protocol!=="https:")throw new Error("Ungültige Stream-Adresse.");for(let e=0;e<3;e++){const r=new AbortController;const i=setTimeout(()=>r.abort(),3e4);const s=AbortSignal.any([o.signal,r.signal]);try{const e=await fetch(n.href,{credentials:n.origin===location.origin?"include":"omit",signal:s});if(!e.ok){await(e.body?.cancel());if(e.status===401||e.status===403||e.status===404)throw Object.assign(new Error(`Stream-Zugriff verweigert oder abgelaufen (HTTP ${e.status}). Aufnahme neu laden und erneut versuchen.`),{noRetry:true});throw new Error(`Stream-Anfrage fehlgeschlagen (HTTP ${e.status}).`)}const r=e.headers.get("content-type")||"";if(/text\/html/i.test(r)){await(e.body?.cancel());throw Object.assign(new Error("SharePoint liefert eine Webseite statt Videodaten. Anmeldung und Wiedergabezugriff prüfen."),{noRetry:true})}return t?await e.text():new Uint8Array(await e.arrayBuffer())}catch(t){if(o.signal.aborted||t.noRetry||e===2)throw t}finally{clearTimeout(i)}}};try{if(location.protocol!=="https:"||!/\.sharepoint\.(com|us|de|cn)$/i.test(location.hostname)){throw new Error("Die Aufnahme direkt in SharePoint/Stream öffnen und dort das Skript ausführen.")}const n=performance.getEntriesByType("resource").map(e=>e.name);const r=[...new Set(n.filter(e=>{try{return/\/videomanifest$/i.test(new URL(e).pathname)&&new URL(e).searchParams.get("format")==="dash"}catch{return false}}))];if(!r.length)throw new Error("Kein DASH-Manifest gefunden. Video kurz abspielen, pausieren und das Skript erneut ausführen.");const m=r[r.length-1];const b=(new DOMParser).parseFromString(await E(m,true),"application/xml");const h=b.documentElement;if(h.localName!=="MPD"||d(b,"parsererror").length)throw new Error("Das Video-Manifest konnte nicht gelesen werden.");if(h.getAttribute("type")!=="static")throw new Error("Dieses Skript unterstützt fertiggestellte Aufnahmen. Live-Streams werden nicht unterstützt.");const g=a(h,"Period");if(g.length!==1)throw new Error("Aufnahmen mit mehreren DASH-Perioden werden noch nicht unterstützt.");const p=g[0];if(p.getAttribute("start")&&c(p.getAttribute("start"))!==0)throw new Error("DASH-Perioden mit versetztem Start werden noch nicht unterstützt.");const S=c(p.getAttribute("duration")||h.getAttribute("mediaPresentationDuration"));if(!Number.isFinite(S)||S<=0)throw new Error("Die Aufnahme hat keine gültige Dauer.");let k;const $=[];for(const e of["video","audio"]){const t=a(p,"AdaptationSet").filter(t=>t.getAttribute("contentType")===e||(t.getAttribute("mimeType")||"").startsWith(e+"/"));if(!t.length)throw new Error(`Keine ${e==="video"?"Bildspur":"Tonspur"} gefunden.`);const n=t[0];const r=a(n,"Representation").sort((e,t)=>Number(t.getAttribute("bandwidth"))-Number(e.getAttribute("bandwidth")));const i=r[0];if(!i)throw new Error("Keine abspielbare Stream-Qualität gefunden.");const o=[h,p,n,i];let c=new URL(m);const u={};let f;for(const e of o){const t=s(e,"BaseURL");if(t)c=new URL(t.textContent.trim(),c);const n=s(e,"SegmentTemplate");if(n){for(const e of n.attributes)u[e.name]=e.value;f=s(n,"SegmentTimeline")||f}}if(!f||!u.initialization||!u.media)throw new Error("Dieses DASH-Segmentformat wird noch nicht unterstützt.");if(Number(u.presentationTimeOffset||0)!==0)throw new Error("Versetzte DASH-Zeitachsen werden noch nicht unterstützt.");const b=Number(u.timescale||1);if(!Number.isSafeInteger(b)||b<=0)throw new Error("Ungültige Segment-Zeitbasis.");let g,w;const y=[...a(n,"ContentProtection"),...a(i,"ContentProtection")];if(y.length){const e=y.flatMap(e=>d(e,"SegmentEncryption"));const t=y.flatMap(e=>d(e,"CryptoPeriod"));if(e.length!==1||!/aes128-cbc/i.test(e[0].getAttribute("schemeIdUri")||"")||t.length!==1){throw new Error("Dieses Verschlüsselungsformat wird nicht unterstützt. Unterstützt ist die AES-CBC-Segmentverschlüsselung des SharePoint-Players.")}const n=window.g_fileInfo?.EncKey;const r=typeof n==="string"?JSON.parse(n):n;if(!r?.Key)throw new Error("Der Player-Schlüssel ist auf dieser Seite nicht verfügbar. Video kurz abspielen und erneut versuchen.");k||=await crypto.subtle.importKey("raw",l(r.Key),{name:"AES-CBC"},false,["decrypt"]);w=k;const i=(t[0].getAttribute("IV")||"").replace(/^0x/,"");if(!/^[a-f\d]{32}$/i.test(i))throw new Error("Keine unterstützte AES-IV im Manifest gefunden.");g=Uint8Array.from(i.match(/../g),e=>parseInt(e,16))}const v=(e,t,n)=>{const r="^@";const o=e.replace(/\$\$/g,r).replace(/\$(RepresentationID|Bandwidth|Time|Number)(?:%0(\d+)d)?\$/g,(e,r,o)=>{const s={RepresentationID:i.getAttribute("id"),Bandwidth:i.getAttribute("bandwidth"),Time:t,Number:n}[r];return o?String(s).padStart(Number(o),"0"):String(s)});if(/\$[^$]+\$/.test(o))throw new Error("Unbekannter Platzhalter im Segment-Link.");return new URL(o.replaceAll(r,"$"),c).href};const A=a(f,"S");const x=[];let N=0,P=Number(u.startNumber||1);for(let t=0;t<A.length;t++){const n=A[t];if(n.hasAttribute("t"))N=Number(n.getAttribute("t"));const r=Number(n.getAttribute("d"));let i=Number(n.getAttribute("r")||0);if(i===-1){const e=A[t+1]?.hasAttribute("t")?Number(A[t+1].getAttribute("t")):S*b;i=Math.ceil((e-N)/r)-1}if(!Number.isSafeInteger(N)||!Number.isSafeInteger(r)||r<=0||!Number.isSafeInteger(i)||i<0||x.length+i+1>1e5)throw new Error("Ungültige oder zu große DASH-Zeitachse.");for(let t=0;t<=i;t++,N+=r,P++){x.push({url:v(u.media,N,P),time:N/b,end:(N+r)/b,kind:e})}}const D=async e=>{let t=await E(e);const n=(new TextDecoder).decode(t.subarray(4,8));if(w&&!["ftyp","styp","moof","sidx"].includes(n)){try{t=new Uint8Array(await crypto.subtle.decrypt({name:"AES-CBC",iv:g},w,t))}catch{throw new Error("Videosegment konnte nicht entschlüsselt werden. Aufnahme neu laden und erneut versuchen.")}}return t};$.push({kind:e,segments:x,decode:D,init:await D(v(u.initialization,0,Number(u.startNumber||1))),bandwidth:Number(i.getAttribute("bandwidth"))||0})}i.duration=S;const N=$.find(e=>e.kind==="video"),P=$.find(e=>e.kind==="audio");e(N.init,P.init,S);const D=async n=>{if(["downloading","selecting"].includes(i.status)||A)return;o=new AbortController;i.status="selecting";w.disabled=y.disabled=true;let r;const s=[];let a;try{if(n==="file"&&window.showSaveFilePicker){const e=await window.showSaveFilePicker({suggestedName:u,types:[{description:"MP4-Video",accept:{"video/mp4":[".mp4"]}}]});r=await e.createWritable()}else{const e=S*(N.bandwidth+P.bandwidth)/8;if(e>768*1024*1024)throw new Error("Die Aufnahme ist für den Download im Arbeitsspeicher zu groß. Bitte „MP4 speichern“ mit Dateidialog verwenden.")}if(A||o.signal.aborted)throw new DOMException("Abgebrochen","AbortError");i.status="downloading";i.completed=i.bytes=0;w.disabled=y.disabled=true;v.textContent="Abbrechen";const d=$.flatMap(e=>e.segments.map(t=>({...t,track:e}))).sort((e,t)=>e.time-t.time||e.kind.localeCompare(t.kind));i.total=d.length;const c=e(N.init,P.init,S);const l=async e=>{i.bytes+=e.length;if(r)await r.write(e);else{if(i.bytes>768*1024*1024)throw new Error("Arbeitsspeicher-Limit erreicht. Bitte über den Dateidialog speichern.");s.push(e)}};await l(c.init);for(let e=0;e<d.length;e+=4){if(o.signal.aborted)throw new DOMException("Abgebrochen","AbortError");const t=d.slice(e,e+4);const n=await Promise.all(t.map(e=>e.track.decode(e.url)));for(let e=0;e<t.length;e++){await l(c.fragment(n[e],t[e].kind));i.completed++}x(`${Math.round(i.completed/i.total*100)} %: ${i.completed}/${i.total} Segmente, ${(i.bytes/1024/1024).toFixed(1)} MiB. Bild und Ton werden zusammengefügt.`)}if(o.signal.aborted)throw new DOMException("Abgebrochen","AbortError");if(r){await r.close();r=null;i.status="saved";x(`Gespeichert: ${u} (${(i.bytes/1024/1024).toFixed(1)} MiB). Bild und Ton sind in einer MP4 enthalten.`)}else{const e=new Blob(s,{type:"video/mp4"});s.length=0;a=URL.createObjectURL(e);const t=document.createElement("a");t.href=a;t.download=u;t.textContent="MP4 erneut herunterladen";t.style.display="block";f.appendChild(t);t.click();i.status="download-ready";x(`MP4 fertig: ${(i.bytes/1024/1024).toFixed(1)} MiB, mit Bild und Ton. Der Brave-Download wurde angestoßen; Abschluss in Brave prüfen.`);setTimeout(()=>{URL.revokeObjectURL(a);t.remove()},10*60*1e3)}console.info(`${t} ${i.message}`)}catch(e){const n=e.name==="AbortError";o.abort();if(r)await r.abort().catch(()=>{});s.length=0;i.status=n?"cancelled":"error";x(n?"Abgebrochen. Es wurde keine vollständige MP4 gespeichert.":e.message);if(!n)console.error(`${t} ${e.message}`)}finally{w.disabled=y.disabled=false;v.textContent="Schließen"}};w.addEventListener("click",()=>D("file"));y.addEventListener("click",()=>D("memory"));w.disabled=y.disabled=false;i.status="ready";i.total=N.segments.length+P.segments.length;x(`Bereit: ${(S/60).toFixed(1)} Minuten, ${i.total} Segmente. „MP4 speichern“ schreibt direkt in eine Datei. „Über Brave herunterladen“ sammelt die MP4 im Arbeitsspeicher.`);console.info(`${t} ${i.message}`)}catch(e){o.abort();i.status="error";x(e.message);console.error(`${t} ${e.message}`)}})().catch(e=>console.error(`[SharePoint video] ${e.message}`));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment