param( $Url, $CookiesFromBrowser = "firefox" ) while (1) { <# get format info #> while (1) { Write-Host "Checking if stream is live..." $x = yt-dlp --cookies-from-browser $CookiesFromBrowser -F $Url if ($LASTEXITCODE -eq 0) { write-warning "Live!" break; } Write-Warning "Not live, waiting 5 minutes >> $ww" sleep (60*5) } Write-Host "Ripping formats with yt-dlp..." $InfoJson = yt-dlp --cookies-from-browser $CookiesFromBrowser --dump-json $Url | ConvertFrom-Json $VideoId = $InfoJson.id $DvrVideoFmt = $InfoJson.formats | ? { $_.format_id -ilike "*dvr*" } | select-object -last 1 $DvrAudioFmts = $InfoJson.formats | ? { $_.format_id -ilike "*dvr*audio*" } # TODO: DVR video format may sometimes also carry audio with it, take into account vvv if (( $DvrAudioFmts | ? { $_.format_id -ilike '*bypass*' } )) { $DvrAudioFmt = $DvrAudioFmts | ? { $_.format_id -ilike '*bypass*' } | select-object -last 1 } else { $DvrAudioFmt = $DvrAudioFmts | select-object -last 1 Write-Warning "DVR bypass audio format not available. Using potentially lower quality $($DvrAudioFmt.format_id) format!" } if ($null -eq $DvrVideoFmt) { Write-Warning "The DVR video format isn't available yet/anymore. Cannot proceed!!! Is the stream live?" exit 1 } elseif ($null -eq $DvrAudioFmt) { Write-Warning "The DVR video format isn't available yet/anymore. Cannot proceed!!! Is the stream live?" exit 1 } break; } Write-Host "Selected formats (v)$($DvrVideoFmt.format_id) + (a)$($DvrAudioFmt.format_id)" <# ### ### ### #> $Headers = @{ 'User-Agent' = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36' 'Accept' = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' 'Accept-Language' = 'en-us,en;q=0.5' 'Sec-Fetch-Mode' = 'navigate' 'Referer' = 'https://www.openrec.tv/' } $BinFiles = (("v@"+$DvrVideoFmt.format_id), $DvrVideoFmt),(("a@"+$DvrAudioFmt.format_id), $DvrAudioFmt) | % -Throttle 2 -Parallel { $Hs = $using:Headers $existing = [System.Collections.Concurrent.ConcurrentBag[Int32]]::new() $VideoId = $using:VideoId $ThreadId = $_[0] $Fmt = $_[1] $Id = $Fmt.format_id $Playlist = $Fmt.url $LocalPlaylistFile = "$VideoId--$Id.m3u8" $FragUrlBase = $Playlist -replace 'chunklist(_\d.*|\.m3u8)','' function l([Switch]$D, [Switch]$W,[Switch]$E, $Message) { if ($E) { throw "[$ThreadId]: $Message" } elseif ($W) { Write-Host "[$ThreadId]: $Message" -Foreground Yellow } elseif ($D) { Write-Host "[$ThreadId]: $Message" -Foreground DarkGray } else { Write-Host "[$ThreadId]: $Message" } } # like 0/seg_____llhls.m4s filter parse-fragmentname() { if ($_ -like "*.m4s") { $_ -match '(?<=0\/seg_\d_)\d+' | out-null } else { $_ -match '\d+(?=\.(?:ts|aac)$)' |out-null } $FragLinkName = $_ -split '/',2 | select-object -last 1 $raw = $matches[0] return @{ Link=$_; Id=[int]::parse($Raw); Filename=$FragLinkName } } try { l "reading $VideoId--$ThreadId.finished" $PreexistingFragments = gc "$VideoId--$ThreadId.finished" -ea stop l "found $( $PreexistingFragments.Count ) preexisting fragments, skipping them..." $PreexistingFragments | % { $existing.Add( [int]::parse($_) ) } } catch { l "found no preexisting fragments" # first run, dont worry } l "hello for list $Playlist" new-item -type dir -name "$VideoId--$Id" -force | out-null $StreamHasEnded = $false #$StreamHasEnded = $true # true for testing <# initial playlist download #> try { if (-not $StreamHasEnded) { iwr -Headers $Hs $Playlist -Outfile $LocalPlaylistFile -SessionVar Session } else { # testing as mentioned still needs $session which is also now made here iwr -Headers $Hs $using:Url -SessionVar Session | Out-Null } } catch { l -E "Failed to get playlist $Playlist. Cannot proceed!" } <# SOME playlists are m4s, some are aac+ts #> $Filetype = (gc $LocalPlaylistFile | select-object -first 20 | ? { $_ -notmatch '^#' } | select-object -first 1) -split '\.' | select-object -last 1 if ($Filetype -eq 'm4s') { <# get m4s init fragment #> $InitFrag = (gc $LocalPlaylistFile | select-object -first 20 | ? { $_ -like '#EXT-X-MAP:URI=*' } ) -split '=',2 -replace '^.|.$','' | select-object -last 1 $InitFragName = $InitFrag -replace '^0\/','' l "Found M4S, init=$InitFragName" try { iwr -WebSession $Session "$FragUrlBase$InitFrag" -OutFile "$VideoId--$Id/a-$InitFragName" } catch { l -E "Failed to get init fragment, cannot proceed!!!" } } else { # no init for aac+ts formats l "Found $Filetype" } <# loop and download fragments #> $NoNewFragmentRoundAttemptsRemaining = 10 while (1) { if (-not $StreamHasEnded) { <# if playlist fails, retry a few times in case of network issues #> $refreshes = 10 while ( $refreshes -gt 0) { try { l -D "Refreshing playlist... ($refreshes attempts remaining)" iwr -WebSession $Session $Playlist -Outfile $LocalPlaylistFile break; } catch { $refreshes-- sleep 6 # no continue, fall through to next if() when <=0 } } if ($refreshes -le 0) { l -W "Failed to refresh playlist 10 times, assuming stream has ended" $StreamHasEnded = $true continue; } if ( (gc $LocalPlaylistFile -Raw) -like "*#EXT-X-ENDLIST*") { l -W "Found EXT-X-ENDLIST marker in playlist; stream has ended!" $StreamHasEnded = $true continue; } } $AllFragments = get-content $LocalPlaylistFile | ? { $_ -notmatch '^#' } <# filter out what we already have #> $PendingFragments = $AllFragments | parse-fragmentname | ? { $_.Id -notin $existing } # pending will unexpectedly be a hashtable instead of hashtable[] when the ? clause returns only one element if ( $null -ne $PendingFragments -and $PendingFragments.GetType() -eq [Hashtable] ) { $PendingFragments = @($PendingFragments) } $s = (2 + (get-random -min -1.0 -max 1.0)) <# check if playlist has stagnated, on 10 straight refreshes and stagnations (60s), break #> if ($null -eq $PendingFragments) { if (-not $StreamHasEnded) { if ($NoNewFragmentRoundAttemptsRemaining -le 0) { l -W "No new fragments found in the last 10 rounds! Assuming the stream has ended..." $StreamHasEnded = $true continue; } $NoNewFragmentRoundAttemptsRemaining-- l -W "No new fragments! $NoNewFragmentRoundAttemptsRemaining attempts remaining..." l -D "playlist: sleeping $s" sleep 6 continue; } else { l "No new fragments and the stream has been marked as over. Exiting thread, post-processing will continue after both threads exit." break; } } <# found some fragments... #> l "Found $( $PendingFragments.Count ) new fragments: $( ($PendingFragments.Id) -join ',' )" # ok this can actually be pretty small, the website hits its playlist like every 2 seconds # smaller = refresh playlist sooner, less chance of missing some fragnents at the end $FragmentAmount = 6 $PendingFragments = $PendingFragments | Select-Object -first $FragmentAmount l "Selecting earliest $( [math]::min($FragmentAmount, $PendingFragments.Count) ) fragments to run: $( ($PendingFragments.Id) -join ',' )" <# no new fragments? #> if ($PendingFragments.Count -eq 0) { if ($NoNewFragmentRoundAttemptsRemaining -le 0) { l -W "No new fragments found in the last 10 rounds! Assuming the stream has ended..." $StreamHasEnded = $true continue; } $NoNewFragmentRoundAttemptsRemaining-- l -W "No new fragments! $NoNewFragmentRoundAttemptsRemaining attempts remaining" l -D "playlist: sleeping $s" sleep ($s*2) continue; } $NoNewFragmentRoundAttemptsRemaining = 10 <# download new fragments #> $PendingFragments | % -throttle ($FragmentAmount/2) -par { $FragmentData = $_ $ThreadId = $using:ThreadId $FragLink = $FragmentData.Link $FragLinkName = $FragmentData.Filename $FragmentID_Int = $FragmentData.Id $FragmentID_Str = "{0:d5}" -f $FragmentID_Int function l([Switch]$D, [Switch]$W,[Switch]$E, $Message) { if ($E) { throw "[$ThreadId]: $Message" } elseif ($W) { Write-Host "[$ThreadId]: $Message" -Foreground Yellow } elseif ($D) { Write-Host "[$ThreadId]: $Message" -Foreground DarkGray } else { Write-Host "[$ThreadId]: $Message" } } l -D "Downloading fragment $FragmentID_Str : $using:FragUrlBase$FragLink" iwr -WebSession $using:Session "$using:FragUrlBase$FragLink" -outfile "$using:VideoId--$using:Id/b-$FragmentID_Str.$using:Filetype" ($using:existing).Add( $FragmentID_Int ) } $existing -join "`n" | out-file -encoding utf8nobom -nonewline "$VideoId--$ThreadId.finished" -ea ignore l -D "playlist: sleeping $s" sleep $s } <# finished downloading!! post-process #> l "Merging fragments..." gci "$VideoId--$Id/*.$Filetype" | gc -AsByteStream -ReadCount 0 | set-content -AsByteStream "$VideoId--$Id/merged.bin" return @{Filename="$VideoId--$Id/merged.bin"; MediaType=( $ThreadId[0] )} } $VideoOffset = 0.0 $VideoFile = $null $AudioOffset = 0.0 $AudioFile = $null # fix desync $BinFiles | % { $I = $_ write-host ($I|out-default) if ($_.MediaType -eq 'v') { $VideoOffset = (ffprobe -v error -show_streams -show_format $I.Filename) | ? { $_ -like "start_time=*" } | Select-Object -first 1| % replace start_time= '' $VideoFile = $_.Filename } else { $AudioOffset = (ffprobe -v error -show_streams -show_format $I.Filename) | ? { $_ -like "start_time=*" } | Select-Object -first 1| % replace start_time= '' $AudioFile = $_.Filename } } Write-Host "voff=$VideoOffset aoff=$AudioOffset calculated=$(-1 * ($VideoOffset - $AudioOffset))" ffmpeg -v error -y -itsoffset "$(-1 * ($VideoOffset - $AudioOffset))" -i $AudioFile -i $VideoFile -c copy "$VideoId.mp4" ffmpeg -v error -y -itsoffset "$($VideoOffset - $AudioOffset)" -i $AudioFile -i $VideoFile -c copy "$VideoId-poffset.mp4" gi "$VideoId.mp4","$VideoId-poffset.mp4" Write-Host "$VideoID.mp4 should be correct, but there is a small chance the audio offset is backwards. If audio is desynced, check $VideoId-poffset.mp4" exit