Skip to content

Bulk video conversion using PowerShell script

This Windows PowerShell script performs bulk video files compression with result filename configuration.

It is supplied with an additional Windows Batch script to run from the command line easily and a Windows Registry script to configure execution from the Windows folder background context menu.

Features

  • Conversion to mp4 format
  • Multiple input file path wildcard patterns in a single invocation
  • 90, 180 and 270 degree clockwise rotation
  • Resizing with a single maximum length of the video frame for both horizontal and vertical sides
  • Truncation using start and end time points
  • Awareness of slow motion videos where the real frame rate is 120/1 fps (e.g. Apple Slow Motion)
  • Output filename prefix presenting the video creation date with an optional instruction to use a specific time zone
  • Optional filename prefix for videos related to Apple Live Photos
  • Modification of creation date/time by specifying delta values for hours, minutes and seconds
  • Configurable output directory
  • Result file last modification time equality to the video creation time
  • Debug mode showing available time zones for usage in filename prefix and detailed video properties for each input file during the process

Overview

All script features are reflected in its input parameters with their default values suitable for some home media library (of cause they can be changed easily). All parameters, default values and some description can be found at the head of the PowerShell script.

The script uses ffmpeg and ffprobe free tools to convert and analyze video files, respectively. These tools should be downloaded separately and unpacked to be ready for usage from the script. Paths to these tools are hard-coded in the script and should be manually changed.

No administration rights required to install or run the tools and the script itself.

The complete set of files:

  • ffmpeg.exe - free tool to convert videos
  • ffprobe.exe - free tool to analyze videos
  • any2mpeg.ps1 - Windows PowerShell script, has hard-coded paths to free tools mentioned above
  • any2mpeg.cmd - Windows Batch script to run Windows PowerShell script mentioned above, has hard-coded path to Windows PowerShell script mentioned above
  • any2mpeg_install.reg - Windows Registry script to add useful Windows folder background context menu item, has hard-coded path to Windows Batch script mentioned above
  • any2mpeg_uninstall.reg - Windows Reistry script to remove Windows folder background context menu item added using script mentioned above

Installation

  • Download and unpack ffmpeg and ffprobe tools from: https://www.ffmpeg.org/download.html
  • Create scripts using sources below
  • Update hard-coded paths to ffmpeg.exe and ffprobe.exe in any2mpeg.ps1 script
  • Update hard-coded path to any2mpeg.ps1 in any2mpeg.cmd script
  • Update hard-coded path to any2mpeg.cmd in any2mpeg_install.reg script
  • Add any2mpeg.cmd parent directory to the PATH Windows user variable in order to be able to use the script without specifying its full location path
  • Execute any2mpeg_install.reg in order to be able to run the script from Windows context menu with default parameters in the current Windows folder with specified output directory

Usage

Below are some some usage examples for video processing.

Example 1

any2mpeg

The above command converts all video files in the current folder with default script parameter values (i.e. max length of video frame side size is 720, creation date and iPhone Live Photo filename prefixes enabled, output folder is the current one).

Example 2

any2mpeg -files IMG001.mov -rotate 90 -startTime 00:00:03 -duration 00:00:06 

This command converts IMG001.mov video file in the current folder with 90 degree clockwise rotation, starts from the 3rd second and 6 seconds long.

Example 3

any2mpeg -files *.avi -noFileNameTimePrefix -noFileNamePhotoPrefix -noUpdateLastWriteTime

This one converts all AVI video files in the current folder without output filename prefixes and file last modification time manipulation.

Example 4

any2mpeg -files *.mov -shiftHours 3 -fileNameTimeZone 'Pacific Standard Time'

And this one converts all MOV video files in the current folder with creation time shift by 3 hours ahead and using standard Pacific time zone in filename prefix.

Example 5

Alt

Windows context menu item converts all video files in the current folder with default parameters and places results into the nested 'out' folder.

Output

The execution progress looks like below:

Parameters:

    files = D:\docs\photo\Import\test
    rotate = 0
    maxSide = 720
    startTime =
    duration =
    shiftHours = 0
    shiftMinutes = 0
    shiftSeconds = 0
    noFileNameTimePrefix = False
    noFileNamePhotoPrefix = False
    fileNameTimeZone = Russian Standard Time
    outDir = D:\docs\photo\Import\test\out
    noUpdateLastWriteTime = False
    debug = False

------------------------------------------------------
Input path: D:\docs\photo\Import\test\IMG_3645.MOV
Input created at: 2017-05-04 20:41:47 +03:00
Input resolution: 1920 x 1080
Input duration: 8.706667 sec
Output created at: 2017-05-04 20:41:47 +03:00
Output size ratio: 0.375
Output path: D:\docs\photo\Import\test\out\20170504-204147-IMG_3645.MOV.mp4
Process time: 8.1214645 sec

------------------------------------------------------
Input path: D:\docs\photo\Import\test\IMG_3672.MOV
Input created at: 2017-05-06 15:42:23 +03:00
Input resolution: 1440 x 1080
Input duration: 3.536667 sec
Output created at: 2017-05-06 15:42:23 +03:00
Output size ratio: 0.5
Output path: D:\docs\photo\Import\test\out\20170506-154223-photo-IMG_3672.MOV.mp4
Process time: 3.7352136 sec

Total process time: 00:00:13.2367571
Press any key to continue . . .

Sources

any2mpeg.ps1

Param (
    [string[]] $files = '*.*', # Multiple values separated by comma. Ex.: 1.mov,2.mp4,somedir/*.mov
    [int] $rotate = 0, # Ex.: 0, 90, 180, 270
    [int] $maxSide = 720,
    [string] $startTime = $Null, # Ex.: 00:00:05.000
    [string] $duration = $Null, # Ex.: 00:01:12.500
    [int] $shiftHours = 0, # 0Ex.: 0, -1, 3
    [int] $shiftMinutes = 0, # Ex.: 0, -1, 3
    [int] $shiftSeconds = 0, # Ex.: 0, -1, 3
    [switch] $noFileNameTimePrefix = $False, # If parameter specified without value, its value is $True
    [switch] $noFileNamePhotoPrefix = $False, # If parameter specified without value, its value is $True
    [string] $fileNameTimeZone = [System.TimeZoneInfo]::Local.Id, # Ex.: 'Russian Standard Time'
    [string] $outDir = $Null,
    [switch] $noUpdateLastWriteTime = $False, # If parameter specified without value, its value is $True
    [switch] $debug = $False # If parameter specified without value, its value is $True
)

# Stop script execution if errors occur
$ErrorActionPreference = "Stop"

# Define tools' locations
$ffmpegExe = 'D:\inst\UTILS\ffmpeg\ffmpeg-20161208-3ab1311-win64-static\bin\ffmpeg.exe'
$ffprobeExe = 'D:\inst\UTILS\ffmpeg\ffmpeg-20161208-3ab1311-win64-static\bin\ffprobe.exe'

# Define ffprobe parameters
$ffprobeParams = '-hide_banner -loglevel fatal'
$ffprobeParamsRawValue = $ffprobeParams + ' -print_format default=noprint_wrappers=1:nokey=1 -select_streams v:0 -show_entries'


# Show actual script parameters
""
"Parameters:"
""
"    files = " + $files
"    rotate = " + $rotate
"    maxSide = " + $maxSide
"    startTime = " + $startTime
"    duration = " + $duration
"    shiftHours = " + $shiftHours
"    shiftMinutes = " + $shiftMinutes
"    shiftSeconds = " + $shiftSeconds
"    noFileNameTimePrefix = " + $noFileNameTimePrefix
"    noFileNamePhotoPrefix = " + $noFileNamePhotoPrefix
"    fileNameTimeZone = " + $fileNameTimeZone
"    outDir = " + $outDir
"    noUpdateLastWriteTime = " + $noUpdateLastWriteTime
"    debug = " + $debug

# Define rotation angle, if required
For ($i = 0; $i -lt ($rotate / 90); $i++) 
{
    $ffmpegParamsRotate = $ffmpegParamsRotate + 'transpose=1,'
}

# Define start point of input movie, if required
if ($startTime -ne $Null -and $startTime -ne '')
{
    $ffmpegParamsStartTime = '-ss ' + $startTime
}
# Define end point of input movie, if required
if ($duration -ne $Null -and $duration -ne '')
{
    $ffmpegParamsDuration = '-t ' + $duration
}

# Get the list of input files excluding directories
$movies = @()
ForEach ($file in $files)
{
    $movies += Get-ChildItem -Path "$file" | Where-Object { !$_.PSIsContainer }
}

# Show available time zones, if in debug mode
if ($debug)
{
    ""
    "----------------"
    [System.TimeZoneInfo]::GetSystemTimeZones() |
        ForEach-Object {'ID = [' + $_.Id + ']       OFFSET = [' + $_.BaseUtcOffset + ']'}
    "----------------"
}

# Remember start time of all items conversion
$scriptProcessStartTime = Get-Date

# Iterate over the input files and perform conversion for each found movie
ForEach ($movie in $movies)
{
    # Skip files without video content (i.e. must be 2 or more frames)
    $duration_ts = (& $ffprobeExe $ffprobeParamsRawValue.Split() stream=duration_ts $movie.FullName)
    if ($duration_ts -notmatch '^[0-9]+$' -or ([int] $duration_ts) -le 1)
    {
        continue
    }

    # Remember start time of the current item conversion
    $itemProcessStartTime = Get-Date

    ""
    "------------------------------------------------------"
    "Input path: " + $movie.FullName

    # Show movie detailed info, if in debug mode
    if ($debug)
    {
        $info = (& $ffprobeExe $ffprobeParams.Split() -show_format -show_streams $movie.FullName)
        "----------------"
        $info
        "----------------"
    }

    # Define movie creation date/time, try to get it from Apple metadata
    $createdAt = (& $ffprobeExe $ffprobeParamsRawValue.Split() format_tags=com.apple.quicktime.creationdate $movie.FullName)
    if ($createdAt -eq $Null)
    {
        # If Apple metadata is not present, try to get it from standard property
        $createdAt = (& $ffprobeExe $ffprobeParamsRawValue.Split() format_tags=creation_time $movie.FullName)
        if ($createdAt -eq $Null)
        {
            # If standard property is not available, use last write date/time of the movie file
            $createdAt = (ls $movie.FullName).LastWriteTime
        }
    }
    $createdAt = [datetime] $createdAt

    "Input created at: " + $createdAt.ToString("yyyy-MM-dd HH:mm:ss zzz")

    # Modify creation date/time by adding hours, minutes and/or seconds, if required
    $createdAt = $createdAt + (New-TimeSpan -Hours $shiftHours -Minutes $shiftMinutes -Seconds $shiftSeconds)

    # Define date/time prefix, if required
    if (!$noFileNameTimePrefix)
    {
        $timeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById($fileNameTimeZone)
        $timePrefix = ([system.TimeZoneInfo]::ConvertTime($createdAt, $timeZone)).ToString("yyyyMMdd-HHmmss") + '-'
    }

    # Define Apple live photo awareness prefix, if required
    if (!$noFileNamePhotoPrefix)
    {
        $photoPrefix = ""
        $livePhoto = (& $ffprobeExe $ffprobeParamsRawValue.Split() format_tags=com.apple.quicktime.content.identifier $movie.FullName)
        if ($livePhoto -ne $Null)
        {
            $photoPrefix = "photo-"
        }
    }

    # Define Apple slow motion (slomo) awareness (where there is a need to 4x slow down video and audio streams)
    $sloMotion4x = ""
    $realFrameRate = (& $ffprobeExe $ffprobeParamsRawValue.Split() stream=r_frame_rate $movie.FullName)
    if ($realFrameRate -ne $Null -and $realFrameRate -match '^120/1$')
    {
        $sloMotion4x = " -filter:v setpts=4*PTS -filter:a atempo=0.5,atempo=0.5"
    }

    # Get movie frame width
    $width = [int] (& $ffprobeExe $ffprobeParamsRawValue.Split() stream=width $movie.FullName)
    # Get movie frame height
    $height = [int] (& $ffprobeExe $ffprobeParamsRawValue.Split() stream=height $movie.FullName)
    # Get movie duration
    $fullDuration = [double] (& $ffprobeExe $ffprobeParamsRawValue.Split() format=duration $movie.FullName)

    # Define scale ratio for the movie according to defined max side size parameter
    $sizeRatio = $maxSide / [math]::Max($width, $height)

    # Define output directory
    $newPath = $movie.Directory.FullName
    if ($outDir -ne $Null -and $outDir -ne '')
    {
        New-Item -ItemType Directory -Force -Path $outDir | Out-Null
        $newPath = $outDir
    }
    # Define output path
    $newPath = $newPath + '\' + $timePrefix + $photoPrefix + $movie.Name + ".mp4"

    "Input resolution: " + $width + " x " + $height
    "Input duration: " + $fullDuration + " sec"
    "Output created at: " + $createdAt.ToString("yyyy-MM-dd HH:mm:ss zzz")
    "Output size ratio: " + $sizeRatio
    "Output path: " + $newPath

    # Deleted previous result of conversion, if present
    if (Test-Path $newPath)
    {
        Remove-Item $newPath
    }

    # Define conversion parameters
    $ffmpegParams = "-loglevel fatal -pix_fmt yuv420p " + 
        $ffmpegParamsStartTime + " " + $ffmpegParamsDuration + 
        " -vf " + $ffmpegParamsRotate + "scale=trunc(iw*" + $sizeRatio + "/2)*2:trunc(ih*" + $sizeRatio + "/2)*2" +
        $sloMotion4x

    # Perform conversion
    & $ffmpegExe -hide_banner -i $movie.FullName $ffmpegParams.Split() -map_metadata 0:g $newPath

    # Set last write date for output movie filem if required
    if (!$noUpdateLastWriteTime)
    {
        (ls $newPath).LastWriteTime = $createdAt
    }

    # Show time taken to process current movie
    "Process time: " + ($(Get-Date) - $itemProcessStartTime).TotalSeconds + " sec"
}

""
"Total process time: " + ($(Get-Date) - $scriptProcessStartTime).ToString()

any2mpeg.cmd

@echo off
powershell -ExecutionPolicy Bypass -NoProfile -Command any2mpeg.ps1 %*

any2mpeg_install.reg

Windows Registry Editor Version 5.00


[HKEY_CURRENT_USER\Software\Classes\Directory\Background\shell\any2mpeg]
@="&any2mpeg to 'out\\...'"
"Icon"="%SystemRoot%\\System32\\shell32.dll,71"

[HKEY_CURRENT_USER\Software\Classes\Directory\shell\any2mpeg]
@="&any2mpeg to its 'out\\...'"
"Icon"="%SystemRoot%\\System32\\shell32.dll,71"


[HKEY_CURRENT_USER\Software\Classes\Directory\Background\shell\any2mpeg\command]
@="D:\\INST\\UTILS\\util\\any2mpeg.cmd -files \"'%V'\" -outDir \"'%V\\out'\" & pause"

[HKEY_CURRENT_USER\Software\Classes\Directory\shell\any2mpeg\command]
@="D:\\INST\\UTILS\\util\\any2mpeg.cmd -files \"'%V'\" -outDir \"'%V\\out'\" & pause"

any2mpeg_uninstall.reg

Windows Registry Editor Version 5.00

[-HKEY_CURRENT_USER\Software\Classes\Directory\Background\shell\any2mpeg]

[-HKEY_CURRENT_USER\Software\Classes\Directory\shell\any2mpeg]