multimedia.media_player のソースコード
"""
PySide6とQtMultimediaを使用したシンプルなメディアプレイヤーアプリケーション。
このモジュールは、メディアファイルの再生、一時停止、シーク、ドラッグ&ドロップ、
およびウィンドウ状態の保存機能を備えた基本的なメディアプレイヤーを提供します。
:doc:`media_player_usage`
"""
import sys
import os
import argparse # argparseをインポート
# PySide6の必要なモジュールをインポート
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QSlider, QFileDialog, QLabel
)
from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput, QMediaFormat
from PySide6.QtMultimediaWidgets import QVideoWidget
from PySide6.QtCore import QUrl, Qt, QSize, Signal
from PySide6.QtGui import QCloseEvent, QDragEnterEvent, QDropEvent # DnDイベント処理に必要
[ドキュメント]
def check_imports(modules):
"""
必要なPythonモジュールがシステムにインストールされているかを確認します。
指定されたモジュールリストを反復処理し、各モジュールがインポート可能か試行します。
インポートに失敗したモジュールがある場合、エラーメッセージとインストールヒントを表示してプログラムを終了します。
:param modules: list[tuple[str, str]] - チェックするモジュール名とインストールヒントのタプルのリスト。
:returns: None - すべてのモジュールがインポートできた場合。インポートできないモジュールがあれば`sys.exit(1)`で終了します。
"""
failed = []
for mod, hint in modules:
try:
# モジュールが存在するかを確認
__import__(mod.split('.')[0], fromlist=["*"])
except ImportError:
failed.append((mod, hint))
if failed:
print("\n❌ 以下のモジュールがインポートできませんでした:\n")
for mod, hint in failed:
print(f"・{mod}\n → インストール方法: {hint}")
sys.exit(1)
# チェック対象モジュールとインストールヒント
required_modules = [
("configparser", "pip install configparser"),
("PySide6.QtWidgets", "pip install PySide6"),
]
check_imports(required_modules)
import configparser
# INIファイル名を設定
INI_FILE = "player_config.ini"
# サポートするメディアファイルのMIMEタイプまたは拡張子を定義
SUPPORTED_MIMES = ["video/mp4", "video/quicktime", "video/x-matroska", "video/x-msvideo"]
SUPPORTED_EXTENSIONS = ["mp4", "mov", "mkv", "avi"]
FILE_DIALOG_FILTER = "Media Files (*.mp4 *.mov *.mkv *.avi);;All Files (*)"
# ----------------------------------------------------------------------
# メインプレイヤーウィンドウ
# ----------------------------------------------------------------------
[ドキュメント]
class QtMediaPlayer(QMainWindow):
"""
メディアファイルの再生、一時停止、シーク、ドラッグ&ドロップ、およびウィンドウ状態の保存機能を備えたPySide6ベースのメディアプレイヤーアプリケーション。
QMediaPlayer、QAudioOutput、QVideoWidgetを使用してメディアを再生します。
UIコントロール(再生/一時停止、停止、巻き戻し、速度調整、ファイルを開く)を提供します。
再生位置スライダーと総再生時間を管理します。
ドラッグ&ドロップでメディアファイルを読み込むことができます。
アプリケーション終了時にウィンドウのサイズと位置をINIファイルに保存し、次回起動時に復元します。
コマンドライン引数によるファイル読み込みと自動再生に対応しています。
:doc:`media_player_architecture`
"""
def __init__(self, media_path=None, autoplay=False):
"""
`QtMediaPlayer` クラスの新しいインスタンスを初期化します。
QMediaPlayerとQAudioOutputを設定し、UI要素を初期化します。
ドラッグ&ドロップ機能を有効にし、ウィンドウのジオメトリをINIファイルからロードします。
起動時にメディアパスが指定されていれば、それをロードします。
:param media_path: str | None - 起動時にロードするメディアファイルのパス。指定しない場合は`None`。
:param autoplay: bool - `media_path`が指定されている場合に自動で再生を開始するかどうか。
:returns: None
"""
super().__init__()
self.setWindowTitle("Media Player (Drop Anywhere)")
# 1. プレイヤー要素のセットアップ
self.player = QMediaPlayer()
self.audio_output = QAudioOutput()
self.player.setAudioOutput(self.audio_output)
# プレイヤーからのシグナルを接続
self.player.durationChanged.connect(self.duration_changed)
self.player.positionChanged.connect(self.position_changed)
# シーク操作中フラグ
self.seeking = False
self.autoplay = autoplay
# --- ウィンドウ全体でドラッグ&ドロップを有効化 ---
self.setAcceptDrops(True)
# 2. UIのセットアップ
self.setup_ui()
# 3. ウィンドウジオメトリのロード
self.load_window_geometry()
# 4. メディアのロードと再生
if media_path:
# 起動時のファイル読み込みでは、autoplay フラグを使用
self.load_media(media_path, self.autoplay)
else:
# ファイルがロードされていない場合は再生ボタンを初期状態に戻す
self.play_pause_button.setText("▶ Play")
self.player.setSource(QUrl()) # ソースをクリア
# --- ドラッグ&ドロップ関連のメソッド (QMainWindow全体で有効) ---
[ドキュメント]
def dragEnterEvent(self, event: QDragEnterEvent):
"""
ドラッグされたアイテムがウィンドウに入ってきたときの処理。
ドラッグされたデータがURLを含み、そのURLがサポートされているメディアファイルの拡張子を持つ場合のみ、
ドロップアクションを受け入れます。これにより、ユーザーはサポートされていないファイルを
ドロップしようとしたときに視覚的なフィードバックを受け取ることができます。
:param event: PySide6.QtGui.QDragEnterEvent - ドラッグアンドドロップイベントに関する情報を含むオブジェクト。
:returns: None
"""
if event.mimeData().hasUrls():
urls = event.mimeData().urls()
if urls:
file_path = urls[0].toLocalFile()
# 拡張子またはMIMEタイプでメディアファイルか簡易チェック
if any(file_path.lower().endswith(f".{ext}") for ext in SUPPORTED_EXTENSIONS):
event.acceptProposedAction()
return
event.ignore()
[ドキュメント]
def dropEvent(self, event: QDropEvent):
"""
ファイルがウィンドウにドロップされたときの処理。
ドロップされたデータからファイルのURLを抽出し、ローカルファイルのパスに変換します。
ファイルが存在することを確認し、そのファイルをメディアプレイヤーにロードして自動的に再生を開始します。
:param event: PySide6.QtGui.QDropEvent - ドロップイベントに関する情報を含むオブジェクト。
:returns: None
"""
if event.mimeData().hasUrls():
urls = event.mimeData().urls()
if urls:
file_path = urls[0].toLocalFile()
if file_path and os.path.exists(file_path):
print(f"File dropped: {file_path}")
# DnDによる読み込みは常に再生を開始
self.load_media(file_path, start_playback=True)
event.acceptProposedAction()
else:
event.ignore()
# --- UI設定 ---
[ドキュメント]
def setup_ui(self):
"""
メディアプレイヤーのユーザーインターフェースをセットアップします。
中央ウィジェット、メインレイアウト、ビデオ表示ウィジェット、シークスライダー、
およびコントロールパネル(ファイルを開く、巻き戻し、停止、再生/一時停止、速度調整)を配置します。
各UI要素のシグナルとスロットを接続します。
:returns: None
"""
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
# 映像表示ウィジェット
self.video_widget = QVideoWidget()
# QVideoWidgetはDnDイベントを処理しないため、QMainWindowのDnDが有効になる
main_layout.addWidget(self.video_widget, stretch=1)
self.player.setVideoOutput(self.video_widget)
# --- シークスライダー ---
self.position_slider = QSlider(Qt.Orientation.Horizontal)
self.position_slider.setRange(0, 0)
self.position_slider.sliderPressed.connect(self.slider_pressed)
self.position_slider.sliderReleased.connect(self.slider_released)
main_layout.addWidget(self.position_slider)
# コントロールパネル
control_layout = QHBoxLayout()
open_button = QPushButton("Open File")
open_button.clicked.connect(self.open_file)
control_layout.addWidget(open_button)
# 巻き戻しボタンを追加
self.rewind_button = QPushButton("⏪ Rewind")
self.rewind_button.clicked.connect(self.rewind_to_start)
control_layout.addWidget(self.rewind_button)
# 停止ボタンを追加
self.stop_button = QPushButton("■ Stop")
self.stop_button.clicked.connect(self.stop_playback)
control_layout.addWidget(self.stop_button)
# 再生/一時停止ボタン
self.play_pause_button = QPushButton("▶ Play")
self.play_pause_button.clicked.connect(self.toggle_play_pause)
control_layout.addWidget(self.play_pause_button)
control_layout.addWidget(QLabel("Speed:"))
self.speed_slider = QSlider(Qt.Orientation.Horizontal)
self.speed_slider.setRange(50, 200) # 0.5倍速から2.0倍速
self.speed_slider.setValue(100)
self.speed_slider.setMinimumWidth(150)
self.speed_slider.valueChanged.connect(self.set_speed)
control_layout.addWidget(self.speed_slider)
main_layout.addLayout(control_layout)
[ドキュメント]
def load_media(self, file_path: str, start_playback: bool = True):
"""
指定されたメディアファイルをプレイヤーにロードし、必要に応じて再生を開始します。
ファイルパスが存在するかどうかを確認し、QMediaPlayerのソースを設定します。
ウィンドウのタイトルを更新し、`start_playback`フラグに基づいてメディアの再生を開始または
一時停止ボタンの状態を設定します。
:param file_path: str - ロードするメディアファイルのパス。
:param start_playback: bool - ファイルロード後に自動で再生を開始するかどうか。デフォルトは`True`。
:returns: None
"""
if os.path.exists(file_path):
self.player.setSource(QUrl.fromLocalFile(file_path))
self.setWindowTitle(f"Media Player - {os.path.basename(file_path)}")
# 再生設定
if start_playback:
self.player.play()
self.play_pause_button.setText("⏸ Pause")
else:
self.play_pause_button.setText("▶ Play")
else:
print(f"Error: File not found at {file_path}")
# --- 制御メソッド ---
[ドキュメント]
def toggle_play_pause(self):
"""
現在のメディアの再生状態(再生中または一時停止中)を切り替えます。
プレイヤーが現在再生中であれば一時停止し、「▶ Play」ボタンのテキストに更新します。
プレイヤーが一時停止中または停止中であれば再生を開始し、「⏸ Pause」ボタンのテキストに更新します。
:returns: None
"""
if self.player.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
self.player.pause()
self.play_pause_button.setText("▶ Play")
else:
self.player.play()
self.play_pause_button.setText("⏸ Pause")
[ドキュメント]
def stop_playback(self):
"""
再生を停止し、開始位置に戻します。
`QMediaPlayer.stop()`メソッドを呼び出して再生を停止します。
再生/一時停止ボタンのテキストを「▶ Play」に更新します。
:returns: None
"""
self.player.stop()
self.play_pause_button.setText("▶ Play")
[ドキュメント]
def rewind_to_start(self):
"""
再生位置を最初に巻き戻します。
`QMediaPlayer.setPosition(0)`を呼び出し、メディアの再生位置をリセットします。
:returns: None
"""
self.player.setPosition(0)
[ドキュメント]
def set_speed(self, value: int):
"""
再生速度を調整します (0.5x - 2.0x)。
スライダーの値(50-200)を受け取り、それを100で割って再生レート(0.5x - 2.0x)を計算します。
計算されたレートを`QMediaPlayer.setPlaybackRate()`に設定します。
:param value: int - 再生速度スライダーの現在の値 (50-200)。
:returns: None
"""
rate = value / 100.0
self.player.setPlaybackRate(rate)
[ドキュメント]
def open_file(self):
"""
ファイルダイアログを開き、メディアをロードします。
`QFileDialog`を使用して、サポートされているメディアファイルのみをフィルタリングして表示します。
ユーザーがファイルを選択してダイアログを閉じると、選択されたファイルを`load_media`メソッドに渡し、再生を開始します。
:returns: None
"""
file_dialog = QFileDialog(self)
file_dialog.setNameFilter(FILE_DIALOG_FILTER)
if file_dialog.exec():
file_path = file_dialog.selectedFiles()[0]
if file_path:
# ユーザー操作による読み込みは常に再生を開始
self.load_media(file_path, start_playback=True)
# --- シーク機能関連のメソッド ---
[ドキュメント]
def slider_pressed(self):
"""
スライダーが押されたら、自動更新を一時停止し、シーク中フラグを立てます。
`seeking`フラグを`True`に設定し、再生位置の自動更新を一時的に停止します。
これにより、ユーザーがスライダーをドラッグしている間、再生位置が不規則にジャンプするのを防ぎます。
:returns: None
"""
self.seeking = True
[ドキュメント]
def slider_released(self):
"""
スライダーが離されたら、再生位置を設定し、シーク中フラグを解除します。
スライダーの現在の値(ミリ秒単位の再生位置)を`QMediaPlayer`に設定します。
`seeking`フラグを`False`に戻し、再生位置の自動更新を再開します。
:returns: None
"""
pos_ms = self.position_slider.value()
self.player.setPosition(pos_ms)
self.seeking = False
[ドキュメント]
def duration_changed(self, duration: int):
"""
メディアの総再生時間 (ミリ秒) が確定したら、スライダーの最大値を設定します。
メディアの総再生時間(ミリ秒)を取得し、再生位置スライダーの最大値をその値に設定します。
これにより、スライダーがメディアの全範囲を正確に表現できるようになります。
:param duration: int - メディアの総再生時間(ミリ秒)。
:returns: None
"""
self.position_slider.setRange(0, duration)
[ドキュメント]
def position_changed(self, position: int):
"""
現在の再生位置 (ミリ秒) が変更されたら、スライダーの現在値を更新します。
`seeking`フラグが`False`の場合(つまり、ユーザーがスライダーを操作していない場合)に、
再生位置スライダーの現在の値を更新します。これにより、再生中にスライダーがリアルタイムで進行します。
:param position: int - メディアの現在の再生位置(ミリ秒)。
:returns: None
"""
if not self.seeking:
self.position_slider.setValue(position)
# --- INIファイルによるウィンドウジオメトリの保存とロード ---
[ドキュメント]
def load_window_geometry(self):
"""
INIファイルからウィンドウの位置とサイズをロードし、適用します。
`player_config.ini`ファイルが存在する場合、その中の`[Window]`セクションから`geometry`設定を読み取ります。
読み取りに成功した場合、ウィンドウのジオメトリを復元します。
ファイルが存在しないか、読み取りに失敗した場合は、画面中央にデフォルトのサイズでウィンドウを配置します。
:returns: None
"""
config = configparser.ConfigParser()
if os.path.exists(INI_FILE):
config.read(INI_FILE, encoding="utf-8")
if "Window" in config and "geometry" in config["Window"]:
geom_str = config["Window"]["geometry"]
try:
x, y, w, h = map(int, geom_str.split(','))
self.setGeometry(x, y, w, h)
return
except Exception:
pass
screen_size = self.screen().availableGeometry().size()
default_width = 800
default_height = 600
default_x = (screen_size.width() - default_width) // 2
default_y = (screen_size.height() - default_height) // 2
self.setGeometry(default_x, default_y, default_width, default_height)
[ドキュメント]
def save_window_geometry(self):
"""
ウィンドウの位置とサイズをINIファイルに保存します。
ウィンドウの現在のジオメトリ(X座標、Y座標、幅、高さ)を取得し、
カンマ区切りの文字列として`player_config.ini`ファイルに書き込みます。
これにより、アプリケーションが次回起動したときにウィンドウの状態が復元されます。
:returns: None
"""
geom = self.geometry()
geom_str = f"{geom.x()},{geom.y()},{geom.width()},{geom.height()}"
config = configparser.ConfigParser()
config["Window"] = {"geometry": geom_str}
try:
with open(INI_FILE, "w", encoding="utf-8") as f:
config.write(f)
except Exception as e:
print(f"Error saving INI file: {e}", file=sys.stderr)
[ドキュメント]
def closeEvent(self, event: QCloseEvent):
"""
ウィンドウが閉じられる際に発生するイベントを処理します。
アプリケーションが終了する前に、現在再生中のメディアを停止します。
ウィンドウの現在のジオメトリをINIファイルに保存し、次回起動時に復元できるようにします。
イベントを受け入れ、ウィンドウの閉鎖を許可します。
:param event: PySide6.QtGui.QCloseEvent - ウィンドウの閉鎖イベントに関する情報を含むオブジェクト。
:returns: None
"""
self.player.stop() # 終了時にプレイヤーを停止
self.save_window_geometry()
event.accept()
[ドキュメント]
def parse_args():
"""
コマンドライン引数を解析し、メディアファイルのパス、自動再生フラグ、ファイル選択ダイアログの表示フラグを取得します。
`argparse`モジュールを使用して、`infile`(オプションのファイルパス)、`--autoplay`、`--ask_open`引数を定義します。
`infile`は起動時にロードするファイルを指定し、`--autoplay`は自動再生を有効にします。
`--ask_open`は、`infile`が指定されていない場合にファイル選択ダイアログを開くかどうかを制御します。
:returns: argparse.Namespace - 解析された引数を含むオブジェクト。
"""
parser = argparse.ArgumentParser(description="PySide6 Media Player")
parser.add_argument(
"infile",
nargs='?',
default=None,
help="起動時に読み込むメディアファイルのパス"
)
parser.add_argument(
"--autoplay",
action="store_true",
default=False,
help="ファイルが指定された場合、自動で再生を開始する"
)
parser.add_argument(
"--ask_open",
action="store_true",
default=False,
help="ファイルが指定されていない場合、ファイル選択ダイアログを表示する"
)
return parser.parse_args()
if __name__ == "__main__":
args = parse_args()
app = QApplication(sys.argv)
initial_path = args.infile
# 1. infileが指定されているが、ファイルが存在しない場合
if initial_path and not os.path.exists(initial_path):
print(f"Warning: Specified file not found: {initial_path}. Proceeding without initial file.")
initial_path = None
# 2. infileが指定されておらず、--ask_openがTrueの場合
if not initial_path and args.ask_open:
file_dialog = QFileDialog()
file_dialog.setWindowTitle("Select Media File")
file_dialog.setNameFilter(FILE_DIALOG_FILTER)
if file_dialog.exec():
initial_path = file_dialog.selectedFiles()[0]
else:
# ユーザーがキャンセルし、起動ファイルがない場合は終了
if not initial_path:
print("No file selected. Exiting.")
sys.exit(0)
# initial_pathがNoneの場合でもウィンドウは開く (メディアなしの状態)
# QtMediaPlayerにautoplayフラグを渡す
window = QtMediaPlayer(initial_path, args.autoplay)
window.show()
sys.exit(app.exec())