Videos, specifically .MP4s, are probably the largest files on your file servers. In part, this is due to the explosion of cheap cameras that record very high definition video. The latest iPhone can record video at 4K yetI have some projectors that can’t show a resolution greater than 720p! Talk about a waste of space.
So, what is a server admin to do? The same thing we did to pictures, shrink some files with PowerShell! Below, you will see a screenshot of the same video before and after conversion!
The script will:
- automatically go through a network share that is used for folder redirection / home folders.
- Convert MP4 files to a 720p resolution. If you want a higher or lower resolution, edit line 255 by changing the preset value.
- Remove the old file if it is larger than the new file.
- Reset all ownership and permissions
- Tag the file so that future conversions run faster.
Conversion happens on the machine running the script. It should be a bit heavy on RAM. Before you get started with this script, do the following:
- Create a folder named ReduceVideoSize in the root of C:\.
- Download Handbrake for Windows and extract HandbrakeCLI.exe to the ReduceVideoSize folder
- Download Taglib. Open the ZIP and copy the folder to C:\ReduceVideoSize
- And as always, make sure that you have tested your backups.
Your folder should look like this picture. You will need to save the script to this folder. Handbrake.log will appear after you run the script one time.
Copy the script below and save it the ReduceVideoSize folder. Edit the middle $UserFolders variable so that it points to your Folder Redirection / Homefolder file share. Edit the where statement to return just one user (double check this by running manually running that line and then seeing what $UserFolders returns).
Once you are comfortable, hit play. In the PowerShell terminal window, you should start seeing HandBrake converting your files. C:\ReduceVideoSize should now show a handbrake.log file. Once a file has finished conversion, you should see the a HandBrake comment under the Details tab in Properties.
If you run Data Deduplication, expect a small uptick in volume usage as the smaller files won’t be deduplicated yet.
#Folder name must have an ending backslash ("C:\ReduceVideoSize\"). This folder should contain your script, HandBrakeCLI.exe, and the taglib-sharp-2.1.0.0-windows folder mentioned below. $HandBrakeFolder = "C:\ReduceVideoSize\" #This script is designed to be ran against a Folder Redirection/Homefolder root share (\\Server\Share\ should contain subfolders named after usernames - \\SERVER\Share\Jmoody\) #Unmark just one of the three lines below. #$UserFolders = Get-ChildItem -Path "\\Server\Share\FR\" $UserFolders = Get-ChildItem -Path "\\Server\Share\FR\" | where name -Like jmoody #$UserFolders = Get-ChildItem -Path "\\Server\Share\FR\" | select -first 5 <# DLL from Here: http://download.banshee.fm/taglib-sharp/2.1.0.0/ Instructions from Here: http://www.toddklindt.com/blog/Lists/Posts/Post.aspx?ID=468 Right click on downloaded ZIP - select Properties - Unblock Extract ZIP to $HandBrakeFolder Used to write extended attributes to converted file. #> [Reflection.Assembly]::LoadFrom((Resolve-path ("$HandBrakeFolder"+"taglib-sharp-2.1.0.0-windows\Libraries\taglib-sharp.dll"))) $HandBrakeEXE = "$HandBrakeFolder" + "HandBrakeCLI.exe" Function Set-Owner { <# .SYNOPSIS Changes owner of a file or folder to another user or group. .DESCRIPTION Changes owner of a file or folder to another user or group. .PARAMETER Path The folder or file that will have the owner changed. .PARAMETER Account Optional parameter to change owner of a file or folder to specified account. Default value is 'Builtin\Administrators' .PARAMETER Recurse Recursively set ownership on subfolders and files beneath given folder. .NOTES Name: Set-Owner Author: Boe Prox Version History: 1.0 - Boe Prox - Initial Version .EXAMPLE Set-Owner -Path C:\temp\test.txt Description ----------- Changes the owner of test.txt to Builtin\Administrators .EXAMPLE Set-Owner -Path C:\temp\test.txt -Account 'Domain\bprox Description ----------- Changes the owner of test.txt to Domain\bprox .EXAMPLE Set-Owner -Path C:\temp -Recurse Description ----------- Changes the owner of all files and folders under C:\Temp to Builtin\Administrators .EXAMPLE Get-ChildItem C:\Temp | Set-Owner -Recurse -Account 'Domain\bprox' Description ----------- Changes the owner of all files and folders under C:\Temp to Domain\bprox #> [cmdletbinding( SupportsShouldProcess = $True )] Param ( [parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)] [Alias('FullName')] [string[]]$Path, [parameter()] [string]$Account = 'Builtin\Administrators', [parameter()] [switch]$Recurse ) Begin { #Prevent Confirmation on each Write-Debug command when using -Debug If ($PSBoundParameters['Debug']) { $DebugPreference = 'Continue' } Try { [void][TokenAdjuster] } Catch { $AdjustTokenPrivileges = @" using System; using System.Runtime.InteropServices; public class TokenAdjuster { [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] internal static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall, ref TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen); [DllImport("kernel32.dll", ExactSpelling = true)] internal static extern IntPtr GetCurrentProcess(); [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] internal static extern bool OpenProcessToken(IntPtr h, int acc, ref IntPtr phtok); [DllImport("advapi32.dll", SetLastError = true)] internal static extern bool LookupPrivilegeValue(string host, string name, ref long pluid); [StructLayout(LayoutKind.Sequential, Pack = 1)] internal struct TokPriv1Luid { public int Count; public long Luid; public int Attr; } internal const int SE_PRIVILEGE_DISABLED = 0x00000000; internal const int SE_PRIVILEGE_ENABLED = 0x00000002; internal const int TOKEN_QUERY = 0x00000008; internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; public static bool AddPrivilege(string privilege) { try { bool retVal; TokPriv1Luid tp; IntPtr hproc = GetCurrentProcess(); IntPtr htok = IntPtr.Zero; retVal = OpenProcessToken(hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok); tp.Count = 1; tp.Luid = 0; tp.Attr = SE_PRIVILEGE_ENABLED; retVal = LookupPrivilegeValue(null, privilege, ref tp.Luid); retVal = AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero); return retVal; } catch (Exception ex) { throw ex; } } public static bool RemovePrivilege(string privilege) { try { bool retVal; TokPriv1Luid tp; IntPtr hproc = GetCurrentProcess(); IntPtr htok = IntPtr.Zero; retVal = OpenProcessToken(hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok); tp.Count = 1; tp.Luid = 0; tp.Attr = SE_PRIVILEGE_DISABLED; retVal = LookupPrivilegeValue(null, privilege, ref tp.Luid); retVal = AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero); return retVal; } catch (Exception ex) { throw ex; } } } "@ Add-Type $AdjustTokenPrivileges } #Activate necessary admin privileges to make changes without NTFS perms [void][TokenAdjuster]::AddPrivilege("SeRestorePrivilege") #Necessary to set Owner Permissions [void][TokenAdjuster]::AddPrivilege("SeBackupPrivilege") #Necessary to bypass Traverse Checking [void][TokenAdjuster]::AddPrivilege("SeTakeOwnershipPrivilege") #Necessary to override FilePermissions } Process { ForEach ($Item in $Path) { Write-Verbose "FullName: $Item" #The ACL objects do not like being used more than once, so re-create them on the Process block $DirOwner = New-Object System.Security.AccessControl.DirectorySecurity $DirOwner.SetOwner([System.Security.Principal.NTAccount]$Account) $FileOwner = New-Object System.Security.AccessControl.FileSecurity $FileOwner.SetOwner([System.Security.Principal.NTAccount]$Account) $DirAdminAcl = New-Object System.Security.AccessControl.DirectorySecurity $FileAdminAcl = New-Object System.Security.AccessControl.DirectorySecurity $AdminACL = New-Object System.Security.AccessControl.FileSystemAccessRule('Builtin\Administrators','FullControl','ContainerInherit,ObjectInherit','InheritOnly','Allow') $FileAdminAcl.AddAccessRule($AdminACL) $DirAdminAcl.AddAccessRule($AdminACL) Try { $Item = Get-Item -LiteralPath $Item -Force -ErrorAction Stop If (-NOT $Item.PSIsContainer) { If ($PSCmdlet.ShouldProcess($Item, 'Set File Owner')) { Try { $Item.SetAccessControl($FileOwner) } Catch { Write-Warning "Couldn't take ownership of $($Item.FullName)! Taking FullControl of $($Item.Directory.FullName)" $Item.Directory.SetAccessControl($FileAdminAcl) $Item.SetAccessControl($FileOwner) } } } Else { If ($PSCmdlet.ShouldProcess($Item, 'Set Directory Owner')) { Try { $Item.SetAccessControl($DirOwner) } Catch { Write-Warning "Couldn't take ownership of $($Item.FullName)! Taking FullControl of $($Item.Parent.FullName)" $Item.Parent.SetAccessControl($DirAdminAcl) $Item.SetAccessControl($DirOwner) } } If ($Recurse) { [void]$PSBoundParameters.Remove('Path') Get-ChildItem $Item -Force | Set-Owner @PSBoundParameters } } } Catch { Write-Warning "$($Item): $($_.Exception.Message)" } } } End { #Remove priviledges that had been granted [void][TokenAdjuster]::RemovePrivilege("SeRestorePrivilege") [void][TokenAdjuster]::RemovePrivilege("SeBackupPrivilege") [void][TokenAdjuster]::RemovePrivilege("SeTakeOwnershipPrivilege") } } foreach ($UserFolder in $UserFolders){ $FullName = $UserFolder.FullName $MP4s = Get-ChildItem -recurse $FullName -include *.mp4 foreach ($MP4 in $MP4s){ #Check if file was previously processed by script $FileInput = $MP4.FullName $media = [TagLib.File]::Create($FileInput) if ($Media.Tag.Comment -ne "HandBrake"){ $Owner = $MP4 | Get-acl | select -ExpandProperty Owner $CurrentName = $MP4.BaseName + $MP4.Extension $FileOutput = $CurrentName + "-NewHB" + $MP4.Extension Set-Location $MP4.DirectoryName write-host "Now Converting: $FileInput" &$HandBrakeEXE --input "$FileInput" --output "$FileOutput" -O --preset-import-gui "Fast 720p30" | Out-File $HandBrakeFolder\Handbrake.log -Append write-host "Finished Creating: $FileOutput" #Ensure that new file was created if ((Get-item "$FileOutput").count -eq 1 -and ((Get-item "$FileOutput").length -lt (Get-item "$FileInput").length)){ Remove-Item $FileInput -Force -Confirm:$False Set-Owner -Path ".\$FileOutput" -Account $Owner Rename-Item -Path ".\$FileOutput" -NewName $CurrentName #Tag File Comment Attribute so that file is skipped on future processing $media = [TagLib.File]::Create($FileInput) $media.Tag.Comment = "HandBrake" $media.Save() #Reset Folder Permission so that Creator Owner ACL is restored icacls.exe .\ /q /c /t /reset } else{ Write-Host "New File is larger than old file. Removing new file." Remove-Item -Path $FileOutput -Force -Confirm:$False } } } write-host "Finished Converting: $FullName" }
How to only select files that are say are > 2Gb in size?
Hello Joseph,
your script encouraged me to tackle the issue of videofiles in our company windows-fileserver with over 13000 files. Based on you sscript i added some features like setting a threshold based on resolution, make a backup copy before converting and make folder exclusions. I also added excessive logging functionality, because i love logging 🙂 I just wanted to say thank you for getting me a foot into the door!
Please share your revisions.
Hi! What is the reason for using Set-Owner?