20160503

PlayOn Stream Recorder Ad Removal

I have been enjoying using PlayOn to record video streams for my Plex server to host, then syncing them to my mobile devices to watch on the bus or when I have some down time, without using my mobile data.

I was a little annoyed with the fact that I needed to use the PlayOn player to skip the ads, as the PlayOn app doesn't let you store the files locally.  To fix this I created a Powershell script that uses ffmpeg to remove the chapters in the mp4 video files that are marked as "Advertisement".  I also trim the first few and last few seconds of the video to remove the annoying PlayOn logo.

It is not perfect, especially for HBO, they have been adding lots of previews/trailers for their content, and PlayOn doesn't recognize it as an ad, but it does a pretty good job on most.

Here is the script, it's a little sloppy but is good enough.
#
# Script.ps1
#
$InputBasePath = "E:\Video\PlayOnRecordings\"
$OutputBasePath = "S:\Video\PlayOnRecordings\"
$vidsToConvert = Get-ChildItem "$InputBasePath*.mp4" -Recurse
#$vidsToConvert = Get-ChildItem "E:\Video\PlayOnRecordings\Xfinity\*.mp4"
foreach ($vid in $vidsToConvert) {
$TARGETFILEPATH = $vid.FullName.Replace($InputBasePath,$OutputBasePath).Replace("PlayOnRecordings","TV Shows")
$TARGETDIR = $TARGETFILEPATH.Replace($vid.Name,"")

if(!(Test-Path -Path $TARGETFILEPATH)){
Write-Output $vid.FullName
New-Item $env:TEMP\MP4ChapterInfo.txt -type file -force
ffprobe -loglevel panic $vid -show_chapters -sexagesimal -print_format csv 1> $env:TEMP\MP4ChapterInfo.txt
$csvVidChapters = Import-Csv $env:TEMP\MP4ChapterInfo.txt -Header @("ObjName","Index","Fraction","StartMiliseconds","StartTime","EndMiliseconds","EndTime","ChapterTitle")

$firstVideo = 1
$lineCount = 1
Write-Output "Chapter Lines: "$csvVidChapters.length
foreach ($line in $csvVidChapters){
$OutputFile = ""
If ([convert]::ToInt32($line.Index) -lt 10) {
$OutputFile = ($env:TEMP + "\temp_" + $line.ChapterTitle + "_0" + $line.Index + $vid.Extension)

}
Else{
$OutputFile = ($env:TEMP + "\temp_" + $line.ChapterTitle + "_" + $line.Index + $vid.Extension)
}

if($firstVideo -eq 1){
Write-Output "Split FIRST Chapter: "$OutputFile
#$newStartTime = ([TimeSpan]::Parse($line.StartTime)).TotalSeconds
#$newStartTime = $newStartTime + 5
#$tsStart =  [timespan]::fromseconds($newStartTime)
#Write-Output "First File, Trim Playon Tag. "("{0:hh\:mm\:ss\,fff}" -f $tsStart)
#$newStartTime = ("{0:hh\:mm\:ss\,fff}" -f $tsStart) -replace ",","."
ffmpeg -loglevel panic -i $vid -ss $line.StartTime -to $line.EndTime -async 1 -acodec copy -vcodec copy $OutputFile
$firstVideo = 0
}
else{
Write-Output "Split Chapter: "$OutputFile
if($lineCount -eq $csvVidChapters.length){
$newEndTime = ([TimeSpan]::Parse($line.EndTime)).TotalSeconds
$newEndTime = $newEndTime - 10
$tsEnd =  [timespan]::fromseconds($newEndTime)
Write-Output "Last File, Trim Playon Tag. "("{0:hh\:mm\:ss\,fff}" -f $tsEnd)
$newEndTime = ("{0:hh\:mm\:ss\,fff}" -f $tsEnd) -replace ",","."
ffmpeg -loglevel panic -i $vid -ss $line.StartTime -to $newEndTime -async 1 -acodec copy -vcodec copy $OutputFile
}
else{
ffmpeg -loglevel panic -i $vid -ss $line.StartTime -to $line.EndTime -async 1 -acodec copy -vcodec copy $OutputFile
}

}
$lineCount++
}
Remove-Item $env:TEMP\MP4ChapterInfo.txt

$vidsToCombine = Get-ChildItem $env:TEMP\temp_Video*.mp4

$count = 0
New-Item $env:TEMP\MP4ConcatList.txt -type file -force
foreach ($vid_Video in $vidsToCombine) {
Write-Output $vid_Video.Length

$VidLength = ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 -sexagesimal $vid_Video
Write-Output "Section Length: "([TimeSpan]::Parse($VidLength)).TotalSeconds

#If ($vid_Video.Length -gt 2000000){
If (([TimeSpan]::Parse($VidLength)).TotalSeconds -gt 30){
Write-Output "Add Concat: "$vid_Video.FullName
"file '" + $vid_Video.FullName + "'" | Out-File $env:TEMP\MP4ConcatList.txt -Append -encoding default
$count++
}
else{
Write-Output "Skip Video Concat: "$vid_Video.FullName
}
Write-Output $count
}


if(!(Test-Path -Path $TARGETDIR )){
Write-Output "Directory does not exist - CREATE DIRECTORY: "$TARGETDIR
New-Item -ItemType directory -Path $TARGETDIR
}

!(Test-Path -Path $TARGETFILEPATH)
$TARGETFILEPATH
If(!(Test-Path -Path $TARGETFILEPATH)){
Write-Output "Count: "$count
If ($count -gt 0){
Write-Output "Build Vid from Chapters..."
ffmpeg -loglevel panic -f concat -i $env:TEMP\MP4ConcatList.txt -c copy $TARGETFILEPATH
}
else{
Write-Output "No Chapters - Copy Vid to: "$TARGETFILEPATH

$newStartTime = 0 #([TimeSpan]::Parse("0:00:00:00,000")).TotalSeconds
$newStartTime = $newStartTime + 5
$tsStart =  [timespan]::fromseconds($newStartTime)
Write-Output "Trim Playon Tag. "("{0:hh\:mm\:ss\,fff}" -f $tsStart)
$newStartTime = ("{0:hh\:mm\:ss\,fff}" -f $tsStart) -replace ",","."

$VidLength = ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 -sexagesimal $vid
Write-Output $VidLength

$newEndTime = ([TimeSpan]::Parse($VidLength)).TotalSeconds
$newEndTime = $newEndTime - 10
$tsEnd =  [timespan]::fromseconds($newEndTime)
Write-Output "Last File, Trim Playon Tag. "("{0:hh\:mm\:ss\,fff}" -f $tsEnd)
$newEndTime = ("{0:hh\:mm\:ss\,fff}" -f $tsEnd) -replace ",","."

ffmpeg -loglevel panic -i $vid -ss $newStartTime -to $newEndTime -async 1 -acodec copy -vcodec copy $TARGETFILEPATH
#Copy-Item $vid -Destination $TARGETFILEPATH
}
}

###Remove-Item $strRemoval -include $vid.Extension
Remove-Item $env:TEMP\MP4ConcatList.txt
Remove-Item $env:TEMP\temp_Video_*.mp4
Remove-Item $env:TEMP\temp_Advertisement_*.mp4

}
else{
Write-Output "Vid Exists Skip: "$vid.FullName
}
}

8 comments:

  1. This script is glorious. Thank you so much for doing the work on this. It doesn't get rid of the loading animation, but it does the rest amazingly.

    ReplyDelete
  2. I've never used Powershell or ffmpeg before, but after recently getting PlayOn, this script gave me reason to! Thanks for this, it's great!

    ReplyDelete
  3. This doesn't seem to work anymore, it adds a 2 second delay to the sound.

    ReplyDelete
  4. here's an updated version. It fixes some problems (audio sync for example) and probably creates some other problems (that I am unaware of).

    Part 1:

    #
    # powershell script to remove Playon tags and/or commercials
    #

    # amount of time at beginning and end of video to trim off video to remove Playon tags
    $startSkipSeconds = 5
    $endSkipSeconds = 6

    # where are the videos to convert
    $inputFolder = "E:\PlayOn\originalPlayons\"

    # where do you want the trimmed videos to be stored
    $outputFolder = "E:\PlayOn\trimmedPlayons\"


    ###################################################################################################################
    # do not change anything below here unless you know what you are doing
    ###################################################################################################################
    $debug = $false
    $deleteTempFiles = $true

    # get a list of videos files to trim
    $videoFilesToProcess = Get-ChildItem "$inputFolder*.mp4" -Recurse

    # for each video file in the list...
    foreach ($videoFile in $videoFilesToProcess) {

    # compute the file path to store the trimmed video
    $outputVideoFilePath = $videoFile.FullName.Replace($inputFolder, $outputFolder)
    $outputVideoFolderPath = $outputVideoFilePath.Replace($videoFile.Name,"")

    # if it already exists, skip it, else continue...
    if (!(Test-Path -Path $outputVideoFilePath)) {

    Write-Output "`r`n- Processing: $($videoFile.FullName)"

    # if the output video folder does not exist, create it.
    if (!(Test-Path -Path $outputVideoFolderPath )) { New-Item -ItemType directory -Path $outputVideoFolderPath | Out-Null }

    # get a table of the chapters in the video
    $chaptersInVideo = & ffprobe -loglevel panic $videoFile -show_chapters -print_format json | ConvertFrom-Json
    $tblChaptersInVideo = $chaptersInVideo.chapters
    $numChaptersInVideo = $tblChaptersInVideo.Length

    if ($debug) { Write-Output "-- Number of chapters: $($numChaptersInVideo)." }

    # if there are any chapters in this video, remove any "Advertisement" chapters and trim off the Playon tags
    if ($numChaptersInVideo -gt 0) {

    # create a temp file to store trimmed video chapter file paths in
    $tmpFile = New-TemporaryFile
    $tmpFileName = $tmpFile.FullName
    $tmpFilePathStart = $tmpFileName.Substring(0, $tmpFileName.LastIndexOf('.'))

    $chapterCount = 1
    foreach ($chapter in $tblChaptersInVideo) {

    $chapterId = $chapter.id

    # compute the initial duration for the chapter
    $startSeconds = $chapter.start / 1000.0
    $endSeconds = $chapter.end / 1000.0
    $durationSeconds = $endSeconds - $startSeconds

    # skip any chapters with the title of "Advertisement" unless they are longer than 5 minutes
    #
    # note: in some cases, chapters are mislabeled and have both video and advertisement in them
    # so if we find any Advertisements that are longer than 5 minutes long, we include them even
    # though the chapter has Advertisements in it so we don't miss some of the show.

    ReplyDelete
  5. part 2:

    $chapterTitle = $chapter.tags.title
    if ($chapterTitle -ne "Advertisement" -OR $durationSeconds -gt 600) {

    # compute the output filename for this chapter
    $outputChapterFile = ($tmpFilePathStart + "_{0:00}" -f $chapterId + "_" + $chapterTitle + $videoFile.Extension)

    # trim off the start and end Playon tags
    if ($chapterCount -eq 1) {
    $startSeconds = $startSeconds + $startSkipSeconds
    if ($debug) { Write-Output "--- trimming $($startSkipSeconds) seconds off start for Playon tag." }
    }
    elseif ($chapterCount -eq $numChaptersInVideo) {
    $endSeconds = $endSeconds - $endSkipSeconds
    if ($debug) { Write-Output "--- trimming $($endSkipSeconds) seconds off end for Playon tag." }
    }

    # compute the duration of the clip
    $durationSeconds = $endSeconds - $startSeconds

    # copy the clipped chapter video to the output temporary video file path
    if ($debug) { Write-Output "-- $($startSeconds)-$($endSeconds) => $($durationSeconds): creating chapter temp file: $($outputChapterFile)." }
    ffmpeg -loglevel panic -ss $startSeconds -i $videoFile -t $durationSeconds -c copy $outputChapterFile
    }
    $chapterCount++
    }

    if ($debug) { Write-Output "-- building trimmed video from chapters => $($outputVideoFilePath)" }

    # load the chapter video file names to concat into the temp file
    $videosToConcat = Get-Item "$($tmpFilePathStart)_*.mp4"
    foreach ($videoToConcat in $videosToConcat) {
    "file '" + $videoToConcat.FullName + "'" | Out-File $tmpFileName -Append -encoding default
    }

    # concat all the chapter video files into the output video file
    ffmpeg -loglevel panic -f concat -safe 0 -i $tmpFileName -c copy $outputVideoFilePath

    # cleanup temporary files
    if ($deleteTempFiles) { Remove-Item $tmpFilePathStart*.* }
    }
    else { # handle video files with no chapters, simply trim off the Playon tags

    # get the original end time
    $durationSecondsOrig = ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 $videoFile
    if ($debug) { Write-Output "-- Original end time: $($durationSecondsOrig)." }

    # compute the new start time
    $startSeconds = $startSkipSeconds
    if ($debug) { Write-Output "--- trimming $($startSkipSeconds) seconds off start for Playon tag." }

    # compute the new duration
    $durationSeconds = $durationSecondsOrig - $startSeconds - $endSkipSeconds
    if ($debug) { Write-Output "--- trimming $($endSkipSeconds) seconds off end for Playon tag." }

    # copy the trimmed video to the output video file path
    if ($debug) { Write-Output "-- $($startSeconds) => $($durationSeconds): creating trimmed video file: $($outputVideoFilePath)." }
    ffmpeg -loglevel panic -ss $startSeconds -i $videoFile -t $durationSeconds -c copy $outputVideoFilePath
    }

    Write-Output "- Completed: $($outputVideoFilePath)"
    }
    else {
    Write-Output "`r`nSKIPPING: output video already exists: $($outputVideoFilePath)"
    }
    }

    ReplyDelete
  6. This comment has been removed by the author.

    ReplyDelete
  7. This comment has been removed by the author.

    ReplyDelete
  8. This comment has been removed by the author.

    ReplyDelete