0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MultiCropPlayer.ps1

Last updated at Posted at 2025-07-24

1. スクリプト概要

スクリプト名: MultiPlayer.ps1
動画ファイルをディスプレイの数だけVLCで同時に再生します
MultiPlayer.ps1の再生時のトリム対応版です

2. 処理と目的

処理の流れ
 1. 2つ以上の動画ファイルを指定します
 2. 手順1で「.」と指定した場合、1つ目に指定したファイルと同じフォルダから類似する名前のファイルを自動で検索し、追加します
 3. 再生ファイルの上下左右のトリミング量をピクセルで指定します
 4. VLCプレイリスト用のデータを作成します
 5. VLCを1つ目は左スピーカーから、2つ目は右スピーカーから出るようにフィルタ設定して起動します
 6. 手順4で起動したVLCにTelnet経由で再生するファイルを送信します
 7. 手順4をディスプレイの数だけ繰り返します
 8. 各VLCで再生される先頭ファイルの無音部分をFFmpegで取得します
 9. 無音部分の差分だけ、片方のVLCの再生を一時停止して同期します
 10. 以下のコマンドに応じて動作します

  • 数字 指定した数値の箇所まで全てのVLCをシークします
  • save 全てのプレイリストファイルをスクリプト内で指定してあるフォルダに保存します
  • . VLCとスクリプトを終了します
  • diff数字 シークする際の差分を指定数字として設定します

目的
同じ楽曲を使用している動画を複数のディスプレイで同時に再生することで、手軽に映像を組み合わせられるようにします。

3. 動作環境と要件

PowerShellのバージョン
7.0以上

OS
Windows10

必要なモジュール
ffmpeg

必要な権限
特になし

その他の設定
特になし

4. 使用方法

基本的な実行方法
スクリプトコードを拡張子ps1で保存してPowershellで実行してください。
ファイルを保存する際は、文字コードをUTF8 BOM付にしてください。

パラメータ
なし

使用例

  1. コマンドラインでpwsh MultiPlayer.ps1を実行
  2. 対象ファイルを2回尋ねられるので、動画ファイルのフルパスを入力

5. スクリプトコード

if( $PSVersionTable.PSVersion.Major -ge 7 -and ((ps -Id $PID | ?{$_.Parent.ProcessName -notin @('pwsh','powershell')}) -ne $null)){
    cls
}

$host.UI.RawUI.WindowTitle = ([IO.Path]::GetFilenameWithoutExtension($PSCommandPath))
$ErrorActionPreference = 'Stop'

# Windows Formsアセンブリをロードし、画面情報を取得
Add-Type -AssemblyName System.Windows.Forms
# 全てのディスプレイ情報を取得
$screens = [System.Windows.Forms.Screen]::AllScreens
# ディスプレイをX座標でソート(左から順に並べるため)
$screens = $screens | sort {$_.Bounds.X}

Write-Host "----------------------------------------------------------------"
Write-Host "ディスプレイ数分VLCを起動して同時再生を行う" -ForegroundColor White -BackGroundColor Black
Write-Host "ファイル名に「.」で同一フォルダで類似ファイル自動選択" -ForegroundColor White -BackGroundColor Black
Write-Host "ファイル名に「+」で同一フォルダで類似ファイル選択画面表示" -ForegroundColor White -BackGroundColor Black
Write-Host "seekSecondに「.」で全プレイヤー終了" -ForegroundColor White -BackGroundColor Black
Write-Host "----------------------------------------------------------------"

# --- 設定変数 ---
# FFmpeg実行ファイルのパス
$ffmpeg = 'D:\ffmpeg\bin\ffmpeg.exe'
# VLC Media Player実行ファイルのパス
$playerExe = 'D:\vlcplayer\vlc.exe'
# Telnet接続先IPアドレス (ローカルホスト)
$ipAddress = 'localhost'
# Telnetポート番号の開始値
$portNumber = 40000
# プレイリストファイルを保存するディレクトリ
$saveDir = 'D:\powershellBatch\VLC\playlist'
# VLCプレイリスト (XSPF形式) のXMLテンプレート
$xmlDoc = [xml]'<?xml version="1.0" encoding="UTF-8"?><playlist xmlns="http://xspf.org/ns/0/" xmlns:vlc="http://www.videolan.org/vlc/playlist/ns/0/" version="1"><title>プレイリスト</title><trackList></trackList><extension application="http://www.videolan.org/vlc/playlist/0"><vlc:item tid="0"/></extension></playlist>'
# リソートのフラグ(このスクリプトでは未使用の可能性あり)
$resort = $true

# ランダムシードを設定し、Telnetパスワードを生成
$null = Get-Random -SetSeed ((Get-Date).Date.GetHashCode())
# 26文字のランダムなTelnetパスワードを生成 (数字、大文字、小文字、アンダースコアを含む)
$telnetPassword = ((1..26) | %{Get-Random -input (@((0..9)|%{$_.ToString()}) + @((65..90)|%{[Char]::ConvertFromUtf32($_)}) + @((97..122)|%{[Char]::ConvertFromUtf32($_)}) + @('_','_','_'))}) -join ''
# 同時再生するVLCの数(ディスプレイ数に合わせる)
$displayCount = 2

# コマンドライン引数からファイルパスを取得し、存在するパスのみをフィルタリング
$files = @($args | %{$_.Trim('"')} | ?{Test-Path -LiteralPath $_})

# ファイルが2つ未満の場合、ユーザーに入力を促すループ
while($files.Count -le 1){
    # 現在選択されているファイルを表示
    for($i=0;$i -lt $files.Count; $i++){
        Write-Host "($($i+1)):$($files[$i])" -ForegroundColor Cyan
    }
    # ユーザーに次のファイルパスの入力を促す
    $inputText = Read-Host "$($files.Count+1)個目のファイル"
    $inputText = $inputText.Trim('"')

    # 入力が「.」または「+」で、かつ既にファイルが1つ以上選択されている場合
    if($inputText -in ('.','+') -and $files.Count -gt 0){
        # 類似ファイル検索用のリストを初期化
        $similarList = New-Object System.Collections.ArrayList
        # 最初のファイルの情報を取得
        $firstfile = $files[0]
        $firstfilename = [IO.Path]::GetFileName($firstfile)

        # 最初のファイルがあるディレクトリ内のファイルをすべて取得(最初のファイル自身は除く)
        $dirFiles = ls -LiteralPath ([IO.Path]::GetDirectoryName($firstfile)) | ?{$_.fullname -ne $firstfile}

        # 類似度計算の最大値(最初のファイル名のUTF8バイト数に基づく)
        $firstfileMaxMatchCount = [Math]::Pow(([Text.Encoding]::UTF8.GetByteCount($firstfilename)),2)
        # ディレクトリ内のファイル数
        $fileCount = $dirFiles.Length
        $fileIndex = 0
        $maxHit = 0 # 最も類似度が高いファイルのヒットポイントを保持

        # ディレクトリ内の各ファイルと比較して類似度を計算
        foreach($comparefile in $dirFiles){
            $hitPercent = 0
            $fileIndex += 1
            # 類似ファイル検索の進捗を表示
            Write-Progress -Activity "類似ファイル検索中...(${fileIndex}/${fileCount})" -Status $comparefile.Name -PercentComplete (100.0*$fileIndex/$fileCount)

            # 名前の短い順に並べ替える(類似文字列検索のため)
            if($firstfilename.Length -gt $comparefile.Name.Length){
                $remainTextList = @($comparefile.Name, $firstfilename)
            } else {
                $remainTextList = @($firstfilename, $comparefile.Name)
            }

            # 同一文字列の数を検索し、一致率としてヒットポイントを計算
            for($len=$remainTextList[0].length; $len -gt 1; $len--){
                for($index=0; ($len+$index) -lt $remainTextList[0].length; $index++){
                    $compareStr = $remainTextList[0].substring($index,$len)

                    $isMatch = $true
                    foreach($remainStr in $remainTextList){
                        if($remainStr.IndexOf($compareStr) -lt 0){
                            $isMatch = $false
                            break
                        }
                    }
                    # 一致した文字列があればヒットポイントに加算し、残りの文字列から削除
                    if($isMatch){
                        for($i = 0 ;$i -lt $remainTextList.Length ; $i++){
                            $remainTextList[$i] = $remainTextList[$i].replace($compareStr,'')
                            $hitPercent += [Math]::Pow(([Text.Encoding]::UTF8.GetByteCount($compareStr)),2)
                        }
                    }
                }
            }
            # 最も類似度が高いファイルを更新
            if($maxHit -lt $hitPercent){
                $maxHit = $hitPercent
                $similarPath = $comparefile.fullname
            }
            # 結果をカスタムオブジェクトとしてリストに追加
            $result = New-Object PSCustomObject -Property @{'fullname'=$comparefile.fullname;'name'=$comparefile.name;'hitpoint'=([int](10000.0*$hitPercent/$firstfileMaxMatchCount))/100}
            $null = $similarList.Add( $result )
        }
        # 進捗表示を完了にする
        Write-Progress -Activity '類似ファイル検索中...' -Status 'complete' -Completed

        # 入力が「+」の場合、ユーザーに似ているファイルから選択させる
        if($inputText -eq '+'){
            $selectItem = $similarList | sort {$_.hitpoint} -Desc | select -first 20 -Property @('hitpoint','name') | Out-GridView -PassThru -Title '同時再生するファイルを選択してください'
            if($selectItem){
                $similarPath = $similarList | ?{$selectItem.name -eq $_.name}  | %{$_.fullname} | select -first 1
            } else {
                # ユーザーが選択しなかった場合、ループの最初に戻る
                continue
            }
        }
        # 選択された類似ファイルをファイルリストに追加
        echo "add path=$($similarPath)"
        $files += $similarPath
    }
    # 入力されたパスが既存のファイルの場合
    elseif( [IO.File]::Exists($inputText) ){
        $files += $inputText
    }
    # 無効なパスが入力された場合
    else {
        Write-Host '無効なパスです。有効なパスもしくは自動検索「.」「+」を入力してください。' -ForegroundColor Yellow
    }
}

# .lnkファイルが指定された場合、ショートカットのリンク先を実際のファイルパスに変換
$newfiles = @()
foreach($filepath in $files){
    if((-not [String]::IsNullOrEmpty($filepath)) -and $filepath.EndsWith('.lnk')){
        $shl = New-Object -ComObject Shell.Application
        $filedata = gi -literalPath $filepath
        $dir = $shl.NameSpace($filedata.DirectoryName)
        $itm = $dir.Items().Item($filedata.Name)
        $newfiles += $itm.GetLink.Path
    } else {
        $newfiles += $filepath
    }
}
$files = $newfiles

# コンソール出力の文字色設定
$TEXT_COLORS = @('Cyan','Green', 'Magenta','Yellow','Blue','Red','White','Gray','Black')
$colorsIndex = 0

# 起動するVLCプレイヤーごとの担当ファイルを表示
echo "playerExe=${playerExe}"
for($i=0;$i -lt $displayCount; $i++){
    $taisho = 0..(($files|Measure).Count-1) | ?{($_ % $displayCount) -eq $i}  | %{$files[$_]}
    $taisho | %{Write-Host $_ -ForegroundColor $TEXT_COLORS[$i] -nonewline;Write-Host "";}
}

# XMLプレイリストに選択されたファイル情報を追加
foreach($var in $files){
    $trackElement = $xmlDoc.CreateElement("track")
    $locationElement = $xmlDoc.CreateElement("location")
    $locationElement.InnerText = $var
    $durationElement = $xmlDoc.CreateElement("duration")
    $durationElement.InnerText = 0
    $extensionElement = $xmlDoc.CreateElement("extension")
    $extensionElement.SetAttribute("application", "http://www.videolan.org/vlc/playlist/0")
    $vlcidElement = $xmlDoc.CreateElement("vlc:id")
    $vlcidElement.InnerText = "0"

    [void]$trackElement.AppendChild($locationElement)
    [void]$trackElement.AppendChild($durationElement)
    [void]$trackElement.AppendChild($extensionElement)
    [void]$extensionElement.AppendChild($vlcidElement)
    foreach( $trackList in $xmlDoc.GetElementsByTagName('trackList')){
        [void]$trackList.AppendChild($trackElement)
    }
}
# デバッグ用に一時フォルダにプレイリストファイルを保存
$xmlDoc.Save( [IO.Path]::Join( ([IO.Path]::GetTempPath()),"multi_$((Get-Date).ToString('yyyyMMddHHmmss')).xspf") )


#上下左右の切り取り量を入力させる
$croplist = @()
do{
  $inputText = Read-Host '上下クロップpx(下はマイナス)(,で区切)'
  if([String]::IsNullOrEmpty($inputText)){
    continue
  }
  
  foreach($txt1 in $inputText.split(',')){
    if([String]::IsNullOrEmpty($txt1)){
      $txt1 = '0';
    }
    
    $i = 0
    $topCropPixel = $null
    foreach($txt2 in $txt1.split('-')){
      $parseValue = 0
      $i += 1
      if([int]::TryParse($txt2,[ref]$parseValue)){
        if($i -eq 1){
          $topCropPixel = $parseValue
          $bottomCropPixel = 0
        }
        if($i -eq 2){
          $bottomCropPixel = $parseValue
          if($topCropPixel -eq $null){
            $topCropPixel = 0
          }
        }
      }
    }
    if($topCropPixel -ne $null){
      $screenRatio = 0.0
      $videoRatio = 16.0/9.0
      if($croplist.Count -lt $screens.Count){
        $screenRatio = 1.0 * $screens[$croplist.Count].Bounds.Width / $screens[$croplist.Count].Bounds.Height
        $screenRatio = $videoRatio - $screenRatio #DEBUG
        $screenRatio = 0.0 #DEBUG
        
      }
      echo "screenRatio=${screenRatio} videoRatio=${videoRatio}"
      $leftCropPixel = [int](($topCropPixel+$bottomCropPixel)*($screenRatio+$videoRatio)/2)
      
      $rightCropPixel = $leftCropPixel
      $croplist += @{'top'=$topCropPixel;'bottom'=$bottomCropPixel;'left'=$leftCropPixel;'right'=$rightCropPixel}
    }
  }
} while($croplist.Count -eq 0)

echo "--------------------"
$croplist | %{Write-Host ($_ | ConvertTo-Json) -Foreground Black -Background White -nonewline;Write-Host ""}
echo "--------------------"



# VLCプロセスのディスクI/Oパフォーマンスカウンターを初期化
$diskCounter = new-object System.Diagnostics.PerformanceCounter("Process","IO Read Bytes/sec","vlc")
$playerStartCount = 0 # スクリプトがVLCを起動した回数
$playerIndex = 0      # 起動するVLCのインデックス

# 既存のVLCプロセスを取得
$processList = ps -Name vlc -ErrorAction SilentlyContinue
if($processList -eq $null){
    $processList = @()
} elseif ($processList -is [System.Diagnostics.Process]){
    $processList = @($processList )
}

# ファイルごとにVLCを起動または既存のVLCにファイルを送信
for($fileIndex = 0; $fileIndex -lt $files.Count; $fileIndex++){
    $videofile = $files[$fileIndex]

    # VLCプロセスがディスプレイ数より少ない場合、新しいVLCを起動
    if((($processList | Measure).Count) -lt $displayCount){
        echo "vlc playing ${fileIndex} ${videofile}"

        $playerArgs = @()
        $playerArgs += ('"'+$videofile+'"') # 再生するビデオファイル
        $playerArgs += '--no-one-instance'    # 複数インスタンスの起動を許可
        $playerArgs += '--extraintf=telnet'   # Telnetインターフェースを有効化
        $playerArgs += '--fullscreen'         # フルスクリーンモードで起動
        
        # 1番目のプレイヤー(インデックス0)に特定のオーディオフィルタを設定
        if($playerIndex -eq 0){
            $playerArgs += '--audio-filter=remap:normvol:compressor' # オーディオフィルタ設定
            $playerArgs += '--aout-remap-channel-left=0' # 左チャンネルを左にリマップ
            $playerArgs += '--aout-remap-channel-right=8' # 右チャンネルをLFEにリマップ(ベース音のみ)
        } 
        # 2番目のプレイヤー(インデックス1)にビデオ反転フィルタとオーディオフィルタを設定
        elseif($playerIndex -eq 1) {
            $playerArgs += '--video-filter=transform{type="hflip"}'  # 映像を水平反転
            $playerArgs += '--audio-filter=remap:normvol:compressor' # オーディオフィルタ設定
            $playerArgs += '--aout-remap-channel-left=8' # 左チャンネルをLFEにリマップ
            $playerArgs += '--aout-remap-channel-right=2' # 右チャンネルを右にリマップ
        }
        $playerArgs += "--qt-fullscreen-screennumber=$($playerIndex%$screens.Count)" # 表示するディスプレイ番号
        $playerArgs += "--telnet-port=$($playerIndex+40000)" # Telnetポート番号
        $playerArgs += "--telnet-password=${telnetPassword}" # Telnetパスワード

        $playerArgs += '--video-filter=croppadd'
        $playerArgs += "--croppadd-croptop=$($croplist[$fileIndex%$croplist.Count]['top'])"
        $playerArgs += "--croppadd-cropbottom=$($croplist[$fileIndex%$croplist.Count]['bottom'])"
        $playerArgs += "--croppadd-cropleft=$($croplist[$fileIndex%$croplist.Count]['left'])"
        $playerArgs += "--croppadd-cropright=$($croplist[$fileIndex%$croplist.Count]['right'])"

        # VLCを起動し、プロセスオブジェクトを$psに格納
        $ps = start $playerExe $playerArgs -PassThru
        # プロセスリストに追加
        $processList += $ps
        $playerIndex += 1

        # 全てのVLCが起動したら、ウィンドウハンドルが取得できるまで待機
        if(($processList | Measure).Count -eq $displayCount){
            echo "Playerの起動待機中..."
            do {
                sleep 0.1 # 0.1秒待機
            } while( ($processList | ?{$_.MainWindowHandle -in ($null,0)}) -ne $null) # メインウィンドウハンドルがnullまたは0の間待機
            echo "WindowHandle=$(($processList|%{$_.MainWindowHandle}) -join ', ')"
        }
        # スクリプトがVLCを起動した数をカウント
        $playerStartCount += 1

    } 
    # 既に必要な数のVLCが起動している場合、Telnetでファイルを送信
    else {
        echo "remote playing ${videofile}"

        # 接続するVLCのTelnetポート番号を計算
        $playerPortNo = $portNumber + ($fileIndex % $displayCount)
        # TCPクライアントを作成し、VLCに接続
        $tc = New-Object System.Net.Sockets.TcpClient
        $tc.connect($ipAddress, $playerPortNo)
        # ネットワークストリームを取得
        $ns = $tc.GetStream()
        $ns.ReadTimeout = 10000 # 読み取りタイムアウト設定 (10秒)
        $ns.WriteTimeout = 10000 # 書き込みタイムアウト設定 (10秒)

        # Telnetパスワード入力待ちプロンプトを待機
        do {
            [byte[]]$resBytes = New-Object byte[] 256
            $readCount = $ns.Read($resBytes, 0, 256) # データ読み込み

            $resMsg = [System.Text.Encoding]::UTF8.GetString($resBytes, 0, [int]$readCount) # バイトを文字列に変換
            echo $resMsg
        } while($resMsg -like 'Password') # 「Password」を含む応答が来るまでループ

        # Telnetパスワードを送信
        $sendMsg = "${telnetPassword}`n"
        $sendBytes = [System.Text.Encoding]::UTF8.GetBytes($sendMsg)
        $ns.Write($sendBytes, 0, $sendBytes.Length)

        # コマンドプロンプト (">") を待機
        do {
            [byte[]]$resBytes = New-Object byte[] 256
            $readCount = $ns.Read($resBytes, 0, 256)
            $resMsg = [System.Text.Encoding]::UTF8.GetString($resBytes, 0, [int]$readCount)
            Write-Host $resMsg -ForegroundColor Yellow
        }while(-not ($resMsg -like '*>*')) # 「>」を含む応答が来るまでループ

        # ファイルを追加またはキューに追加
        if($playerStartCount){
            # 既にプレイヤーが起動している場合、プレイリストにファイルを追加
            $cmd = "enqueue $($videofile)`n"
        } else {
            # 最初のファイルの場合、ファイルを追加して再生
            $cmd = "add $($videofile)`n"
        }
        $sendBytes = [System.Text.Encoding]::UTF8.GetBytes($cmd)
        $ns.Write($sendBytes, 0, $sendBytes.Length)

        # Telnet接続を終了
        $tc.Dispose()
    }
}

# コンソールのウィンドウサイズとバッファサイズを設定
$host.UI.RawUI.WindowSize = New-Object System.Management.Automation.Host.Size(30,5)
$host.UI.RawUI.BufferSize = New-Object System.Management.Automation.Host.Size(30,100)

# スクリプトがVLCを起動した場合、VLCの終了を別スレッドで監視
if($playerStartCount -gt 0){
    # スレッドジョブを開始し、VLCプロセスの終了を待機
    $exitJob = Start-ThreadJob -ScriptBlock {
        $mainprocessId = $args[0]  # メインスクリプトのPID
        $processList = $args[1]    # 監視対象のVLCプロセスリスト

        # 各VLCプロセスが終了するまで待機
        foreach($ps in $processList){
            $ps.WaitForExit()
        }
        # 全てのVLCプロセスが終了したら、メインスクリプトプロセスを強制終了
        Get-Process -Id $mainprocessId | %{$_.kill()}
    } -ArgumentList @($pid,$processList) # メインスクリプトのPIDとVLCプロセスリストを引数として渡す
}

# --- 再生同期処理 ---
# 実際に使用するプレイヤーの数をファイル数とディスプレイ数の少ない方に設定
$playerCount = [Math]::Min($files.Length,$displayCount)

$clientList = @() # TCPクライアントのリスト
$streamList = @() # ネットワークストリームのリスト
# 起動したVLCごとにTelnet接続を確立
for( $i = 0 ; $i -lt $playerCount ; $i++){
    $tc = New-Object System.Net.Sockets.TcpClient
    $tc.connect($ipAddress, $portNumber+$clientList.count) # ポート番号はVLCの起動順に合わせる
    $ns = $tc.GetStream()
    $ns.ReadTimeout = 10000
    $ns.WriteTimeout = 10000

    $clientList += $tc
    $streamList += $ns
}

# 先頭2ファイルの先頭無音時間の差分を取得
$silentDurDiff = 0.0
$noise_max = "-50dB" # 無音と判断するノイズレベル
$duration_min = 0.1 # 無音と判断する最小継続時間
$checkSeconds = "5" # ファイルの先頭5秒をチェック
if($files.Length -ge 2){
    $silentDurList = @()
    # 最初の2つのファイルの無音時間をFFmpegで検出
    for( $i = 0 ; $i -lt 2 ; $i++){
        $videoPath = $files[$i]
        $silentDuration = 0.0
        # FFmpegコマンドを実行して無音区間を検出
        $rawdata = & $ffmpeg "-hide_banner" "-i" "$videoPath" "-vn" "-t" "${checkSeconds}" "-af" "silencedetect=noise=${noise_max}:duration=${duration_min}" "-f" "null" "-" 2>&1
        # 出力から無音終了時間 (silence_end) を抽出
        $silenceText = $rawdata | %{"${_}"} | ?{$_.startsWith('[silencedetect')}
        foreach($txt in $silenceText){
            $reg = [System.Text.RegularExpressions.Regex]::Match($txt, 'silence_end: ([\.0-9]+)')
            # 最初の無音終了時間のみ取得
            if(($reg.Value) -and ($reg.Groups.Count -ge 2) -and ($silentDuration -eq 0.0)){
                $silentDuration = [double]$reg.Groups[1].Value
            }
        }
        $silentDurList += $silentDuration
    }
    # 無音時間の差分を計算
    if($silentDurList.Count -ge 2){
        $silentDurDiff = $silentDurList[0] - $silentDurList[1]
    }
}
echo "silentDurDiff=${silentDurDiff}"

# 無音時差が負の場合、ストリームリストを反転し、差分を正にする
# これにより、無音時間の短い方が先に再生を開始するようになる
if($silentDurDiff -lt 0.0){
    echo "reverse diff"
    [array]::Reverse( $clientList )
    [array]::Reverse( $streamList )
    $silentDurDiff = -$silentDurDiff
}

# 各VLC Telnet接続でパスワードを送信
foreach( $ns in $streamList){
    # パスワード入力待ちを待機
    do {
        [byte[]]$resBytes = New-Object byte[] 256
        $readCount = $ns.Read($resBytes, 0, 256)
        $resMsg = [System.Text.Encoding]::UTF8.GetString($resBytes, 0, [int]$readCount)
        echo $resMsg
    } while($resMsg -like 'Password')

    # パスワードを送信
    $sendMsg = "${telnetPassword}`n"
    $sendBytes = [System.Text.Encoding]::UTF8.GetBytes($sendMsg)
    $ns.Write($sendBytes, 0, $sendBytes.Length)

    # 念のため受信(コマンドプロンプトの応答など)
    [byte[]]$resBytes = New-Object byte[] 256
    $readCount = $ns.Read($resBytes, 0, 256)
    $resMsg = [System.Text.Encoding]::UTF8.GetString($resBytes, 0, [int]$readCount)
    Write-Host $resMsg -ForegroundColor Yellow  -Backgroundcolor Red
}

$seekSecond = 0 # シーク秒数の初期値

# --- コマンド実行ループ ---
:COMMAND_LOOP while(1){
    # VLCに送信するコマンドリスト (コマンド文字列, 実行後の待機時間)
    $commandlist = @(@("play`n",0),@("pause`n",0), @("is_playing`n",0),@("get_time`n",0),@("get_title`n",0), @("seek ${seekSecond}`n",0.5), @("play`n",0))
    
    # コマンドを全VLCに送信し、応答を待つループ
    while(1){
        $timeout = $false # タイムアウトフラグ

        for($commandIndex = 0 ; $commandIndex -lt $commandlist.Count ; $commandIndex++){
            $command = $commandlist[$commandIndex]
            echo "command=$($command[0].Trim()) sleep=$($command[1])"
            $sendMsg = $command[0]     # 送信するコマンド
            $sleepSecond = $command[1] # コマンド送信後の待機時間

            $sendBytes = [System.Text.Encoding]::UTF8.GetBytes($sendMsg)
            foreach($ns in $streamList){
                $ns.Write($sendBytes, 0, $sendBytes.Length)

                # 先頭ファイルの無音時間差分がある場合、最後のプレイコマンド後に差分時間待機
                # これにより、無音の短い方のVLCが先に再生を開始し、同期が図られる
                if( ($silentDurDiff -ne 0.0) -and ($commandIndex -eq ($commandlist.Count-1)) -and ($ns -eq $streamList[0])){
                    echo "sleep silentDurDiff=$([int]($silentDurDiff*1000))ms"
                    sleep -Milliseconds ([int]($silentDurDiff*1000))
                }
            }

            # 各VLCからの応答を受信し、コンソールに表示
            foreach($ns in $streamList){
                [byte[]]$resBytes = New-Object byte[] 256
                $readCount = $ns.Read($resBytes, 0, 256)
                $resMsg = [System.Text.Encoding]::UTF8.GetString($resBytes, 0, [int]$readCount)

                # 空行やプロンプト記号のみの応答でなければ表示
                if( $resMsg -notmatch '^[`n`r`t> ]+$'){
                    Write-Host $resMsg.replace("`n","`t") -ForegroundColor Yellow
                }
            }

            # コマンド実行後の待機処理
            sleep $sleepSecond
            if($sleepSecond -gt 0){
                $idleTime = 0     # アイドル時間
                $retryCount = 0   # リトライカウント

                do {
                    sleep 0.1 # 0.1秒待機
                    $retryCount += 1
                    $diskValue = $diskCounter.NextValue() # VLCプロセスのディスクI/Oをチェック
                    if($diskValue -eq 0){
                        $idleTime += 0.1 # ディスクI/Oがない場合、アイドル時間を加算
                    } else {
                        $idleTime = 0 # ディスクI/Oがある場合、アイドル時間をリセット
                    }
                    # 30回リトライしてもI/Oが安定しない場合、タイムアウト
                    if($retryCount -gt 30){
                        $timeout = $true
                        break
                    }
                } while($idleTime -lt $sleepSecond) # アイドル時間が指定された待機時間に達するまでループ
                if($timeout){
                    break # タイムアウトした場合、内部ループを抜ける
                }
            }
        }
        
        # タイムアウトした場合、外部ループの先頭に戻る
        if($timeout){
            continue
        }
        break # コマンド送信と待機が正常に完了したら内部ループを抜ける
    }

    # スクリプト自身がVLCを起動していない場合(Telnet接続のみの場合)は終了
    if($playerStartCount -eq 0){
        echo "playerStartCount=${playerStartCount}"
        return
    }

    # ユーザーからの操作入力を待機するループ
    do {
        # 操作メニューを表示
        @(@("seek:","White"),@("0~999","Cyan"),@(";保存:","White"),@("save","Cyan"),@(";終了:","White"),@(".","Cyan"),@(";seekdiffSet:","White"),@("diff0~999","Cyan"))| %{Write-Host $_[0] -ForegroundColor $_[1] -NoNewline};Write-Host ''
        $inputText = Read-Host 'seekSecond' # ユーザーに入力を促す

        # 入力が空の場合、シーク秒数を0に設定して次のコマンドループへ
        if([String]::IsNullOrEmpty($inputText)){
            $seekSecond = 0
            break
        } 
        # 「diff数字」形式の場合、動画時差修正値を設定
        elseif($inputText -match '^diff[0-9]{1,5}$'){
            $silentDurDiff = [float]$inputText.Replace('diff','')
            continue # このループを継続し、再度入力を促す
        } 
        # 数字のみの場合、シーク秒数を設定
        elseif($inputText -match '^[0-9]{1,5}$'){
            $seekSecond = [int]$inputText
            break # このループを抜けて、次のコマンドループへ
        } 
        # 「save」の場合、プレイリストファイルを保存
        elseif($inputText -eq 'save'){
            $xmlFullpath = [IO.Path]::Join( $saveDir,"multi_$((Get-Date).ToString('yyyyMMddHHmmss')).xspf")
            $xmlDoc.Save( $xmlFullpath )
            Write-Host "save 「${xmlFullpath}」" -ForegroundColor Yellow
            continue # このループを継続し、再度入力を促す
        } 
        # 「.」の場合、スクリプト全体を終了
        elseif($inputText -eq '.'){
            break COMMAND_LOOP # 外側のCOMMAND_LOOPを抜ける
        }
    }while(1) # 有効な入力があるまでループ
}

# --- 終了処理 ---
# Telnet接続を全て終了
Write-Progress -Activity "exiting Telnet"
foreach( $tc in $clientList){
    $tc.Dispose() # TCPクライアントのリソースを解放
}

# VLCプレイヤープロセスを全て終了
Write-Progress -Activity "exiting vlc"
foreach( $ps in $processList){
    # プロセスがまだ終了していない場合 プロセスを強制終了
    if(-not $ps.HasExited){ 
        $ps.kill()
    }
}
Write-Progress -Completed

6. 注意事項と既知の問題

制約事項
大量のファイルを処理する場合、処理に時間がかかる可能性があります。

既知のバグ
もしバグを発見された場合は、コメントでご報告ください。

トラブルシューティング
・ps1ファイルのエンコーディングには注意してください。

7. 免責事項

本スクリプトにはいかなる保証もありません。使用は自己責任で行ってください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?