Last active
October 5, 2026 12:34
-
-
Save LoggeL/8a43b63341bf453b70d450ccc727110e to your computer and use it in GitHub Desktop.
Teams Video Downloader
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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