pyobjc と wxPython を使用して QTMovieView (および QTMovie) ウィジェットを wxPython パネルに埋め込み、必要な通知を登録しますが、QTMovieDidEndNotification はトリガーされません。
コードのダウンロード可能なバージョン: https://dl.dropboxusercontent.com/u/12781104/QT%20Test%20Download.zip
注目すべきコードは、通知オブザーバーを登録する QuicktimeCtrl クラスの processMovie メソッドにあります。
アプリケーションの終了時にクラッシュが予想されます。現時点では、メモリの解放に関係しているようです。他の問題を解決することに焦点を当てています。
wxPython バージョン 2.9 COCOA が必要です。下位バージョンは Carbon ベースで、NSView の代わりに NSHIObjects (少なくとも私の知る限り、QTMovieView を埋め込むことはできません) を GetHandle 呼び出しで呼び出します。
パネルにまとめられた QT Embed コード
'''
Sublcasses wxMediaCtrl to fix aspect ratio issue
http://forums.wxwidgets.org/viewtopic.php?t=23461&p=100319
Why?
- Yes wx.media.MediaCtrl has a quicktime backend
but it has an issue with the aspect ratio not
correctly displaying (stretches instead of black
bars (seem to be from not setting
PreserveAspectRatio). Also has major issues with
url playback (note only occurs when media is video
mp3s play fine) with only very sporadic and none
reliable ability to play (usually just fails to
load)
'''
import wx
import wx.media
from wx.lib.newevent import NewCommandEvent
import os
print "Import objc items"
# pyobj c stuff
import ctypes
import objc
from Foundation import NSURL, NSString, NSRect, NSDictionary
from AppKit import NSViewWidthSizable, NSViewHeightSizable
print "Importing QTMovie"
from QTKit import QTMovie as M, QTMovieDidEndNotification, QTMovieLoadStateDidChangeNotification, QTMovieView, QTMovieFileNameAttribute, QTMovieOpenAsyncOKAttribute, QTMovieLoadStateAttribute
print "Importing twisted logging module"
from twisted.python import log
# States
# might not be the numbers wx.media.MediaCtrl uses
MEDIASTATE_PLAYING = 0
MEDIASTATE_PAUSED = 1
MEDIASTATE_STOPPED = 2
# mc events are not control events so we get to admit our own "alias" versions
# Events
media_loaded_event , EVT_MEDIA_LOADED = NewCommandEvent()
media_play_event , EVT_MEDIA_PLAY = NewCommandEvent()
media_pause_event , EVT_MEDIA_PAUSE = NewCommandEvent()
media_stop_event , EVT_MEDIA_STOP = NewCommandEvent()
media_finished_event, EVT_MEDIA_FINISHED = NewCommandEvent()
class QuicktimeCtrl(wx.Panel):
def __init__(self, *args, **kw):
wx.Panel.__init__(self, *args, **kw)
self.SetBackgroundColour("BLACK")
ptr = self.GetHandle()
void_ptr = ctypes.c_void_p(ptr)
view = objc.objc_object(c_void_p=void_ptr)
pos = (0, 0)
size = self.GetSize()
r = NSRect(pos, size)
self.mv = QTMovieView.alloc().initWithFrame_(r)
# setup the MacOSX equivalent of sizers
self.mv.setAutoresizesSubviews_(True)
self.mv.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable)
# don't show video controls
self.mv.setControllerVisible_(False)
# preseve aspect ratio
self.mv.setPreservesAspectRatio_(True)
self.stop = True
self.pause = False
view.addSubview_(self.mv)
def _send_event(self, event):
print "Firing an event"
evt = event(self.GetId())
wx.PostEvent(self, evt)
def OnMediaLoaded(self, event):
self._send_event(media_loaded_event)
def OnMediaPlaying(self, event):
self._send_event(media_play_event)
def OnMediaPaused(self, event):
self._send_event(media_pause_event)
def OnMediaFinished(self, event):
self._send_event(media_finished_event)
def OnMediaStopped(self, event):
self._send_event(media_stop_event)
def Load(self, file_path):
# get movie constraints
encoded_path = NSString.alloc().initWithString_(file_path)
dict = NSDictionary.dictionaryWithDictionary_({QTMovieFileNameAttribute : encoded_path,
QTMovieOpenAsyncOKAttribute: False}
)
(movie, error) = M.movieWithAttributes_error_(dict, None)
if movie is None:
print file_path
print "[QT] An error occured"
print error
return False
return self.processMovie(movie)
def LoadURI(self, uri):
encoded_url = NSURL.alloc().initWithString_(uri)
dict = NSDictionary.dictionaryWithDictionary_({QTMovieURLAttribute : encoded_url,
QTMovieOpenAsyncOKAttribute: False}
)
(movie, error) = M.movieWithAttributes_error_(dict, None)
if movie is None:
print uri
print "[QT] An error occured"
print error
return False
return self.processMovie(movie)
def processMovie(self, m):
#m.setDelegate_(self)
# dispose of any old movie and set new one
self.mv.setMovie_(m)
#enum {
# QTMovieLoadStateError = -1L,
# QTMovieLoadStateLoading = 1000,
# QTMovieLoadStateLoaded = 2000,
# QTMovieLoadStatePlayable = 10000,
# QTMovieLoadStatePlaythroughOK = 20000,
# QTMovieLoadStateComplete = 100000L
#};
#typedef NSInteger QTMovieLoadState;
loadState = m.attributeForKey_(QTMovieLoadStateAttribute).longValue()
if loadState == -1:
# error
print "[QT] Error playing media"
return False
if loadState == 1000:
# error in qt as it should be loaded synchnously
print "[QT] Error wans't loaded synchronously"
return False
elif loadState == 2000:
# loaded but not playable
# attach a handler to get when it is playable
# and send load
self.loaded = True
self.playable = False
notf = NSNotificationCenter.defaultCenter()
load_selector = objc.selector(self.OnQTLoad, signature = "v@:@")
end_selector = objc.selector(self.OnQTMovieEnd, signature = "v@:@")
notf.addObserver_selector_name_object_(self, load_selector, QTMovieLoadStateDidChangeNotification, m)
notf.addObserver_selector_name_object_(self, end_selector, QTMovieDidEndNotification, m)
return True
elif loadState >= 10000:
self.loaded = True
self.playable = True
# loaded and playable
# fire evt
wx.CallAfter(self._send_event, media_loaded_event)
return True
return False
def OnQTLoad(self, m):
print "QT LOAD"
loadState = m.attributeForKey_(QTMovieLoadStateAttribute).longValue()
if loadState == -1:
# error
print "[QT] [In Notification] Error playing media"
# send stop event
self._send_event(media_stop_event)
if loadState == 1000:
# error in qt as it should be loaded synchnously
print "[QT] [In Notification] Error wans't loaded synchronously"
self._send_event(media_stop_event)
if loadState >= 10000:
# if now playable
self._send_event(media_loaded_event)
def OnQTMovieEnd(self, notf):
print "QT END"
print "THIS SHOULD BE PRINTED WHEN THE MOVIE ENDS\n\n\n\n\n\n"
# movie is finished
self.Stop() # dont care if succeeds not much we can do otherwise
# then fire finish event
self._send_event(media_finished_event)
def Play(self):
if self.mv.movie() is None:
return False
self.mv.play_(None)
# confirms it works
rate = self.mv.movie().rate()
print "[Play] rate %s " % str(rate)
if rate == 1.0:
self.stop = False
self.pause = False
self._send_event(media_play_event)
return True
else:
return False
def Pause(self):
if self.mv.movie() is None:
return False
self.mv.pause_(None)
rate = self.mv.movie().rate()
print "[Pause] rate %s " % str(rate)
if rate == 0.0:
self.stop = False
self.pause = True
self._send_event(media_pause_event)
return True
else:
return False
def Stop(self):
if self.mv.movie() is None:
return False
# sets it to the beginning; follows wxMediaCtrl that hitting play after starts from the beginning
self.mv.gotoBeginning_(None)
rate = self.mv.movie().rate()
print "[Stop] rate %s " % str(rate)
if rate == 0.0:
self.stop = True
self.pause = False
self._send_event(media_stop_event)
return True
else:
return False
def GetState(self):
if self.stop:
return MEDIASTATE_STOPPED
if self.pause:
return MEDIASTATE_PAUSED
return MEDIASTATE_PLAYING
def SetVolume(self, volume):
if self.mv.movie() is None:
return False
# takes same 0 to 1 value as MediaCtrl so just pass through
# http://docs.wxwidgets.org/2.8/wx_wxmediactrl.html#wxmediactrlsetvolume
self.mv.movie().setVolume(float(volume))
return True
def Tell(self):
return 0
def Length(self):
return 1
def Seek(self, position):
return True
および単純なテストアプリ (MediaCtrl で簡単に置き換えることができますが、問題があります)
# Player
import re
from json import dumps
from urllib import quote
import wxversion
wxversion.select('2.9-osx_cocoa')
import wx
#import wx.media
from wx.lib.buttons import GenBitmapButton as BitmapButton
from quicktime_adapter import QuicktimeCtrl, EVT_MEDIA_STOP, EVT_MEDIA_PLAY, EVT_MEDIA_PAUSE, EVT_MEDIA_LOADED, EVT_MEDIA_FINISHED
CENTER = wx.ALIGN_CENTER | wx.ALL
class Panel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent, -1, size = (400, 500))
#self.url_re = re.compile("^http\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?$")
self.mpc = QuicktimeCtrl(self, -1, style=wx.SIMPLE_BORDER)
self.load_file_button = wx.Button(self, -1, label = "Load File")
self.load_url_button = wx.Button(self, -1, label = "Load URL")
self.play_button = wx.Button(self, -1, label = "Play")
self.pause_button = wx.Button(self, -1, label = "Pause")
self.stop_button = wx.Button(self, -1, label = "Stop")
size = (40, 40)
size_bttn = (50, 50)
#image = wx.Image('play.png', wx.BITMAP_TYPE_ANY).ShrinkBy(10, 10).ConvertToBitmap()
#self.play_button = BitmapButton(self, -1, image, size = size_bttn, style=wx.BORDER_NONE)
#image = wx.Image('pause.png', wx.BITMAP_TYPE_ANY).ShrinkBy(10, 10).ConvertToBitmap()
#self.pause_button = BitmapButton(self, -1, image, size = size_bttn, style=wx.BORDER_NONE)
#image = wx.Image('stop.png', wx.BITMAP_TYPE_ANY).ShrinkBy(10, 10).ConvertToBitmap()
#self.stop_button = BitmapButton(self, -1, image, size = size_bttn, style=wx.BORDER_NONE)
self.play_button.Disable()
bttn_sizer = wx.BoxSizer(wx.HORIZONTAL)
load_buttons_sizer = wx.BoxSizer(wx.VERTICAL)
app_sizer = wx.BoxSizer(wx.VERTICAL)
load_buttons_sizer.Add(self.load_file_button, border = 5, flag = wx.ALL, proportion = 0)
load_buttons_sizer.Add(self.load_url_button, border = 5, flag = wx.ALL, proportion = 0)
#bttn_sizer.Add(self.load_file_button, border = 5, flag = CENTER, proportion = 0)
#bttn_sizer.Add(self.load_url_button, border = 5, flag = CENTER, proportion = 0)
bttn_sizer.Add(load_buttons_sizer, border = 0, flag = wx.ALIGN_LEFT | wx.ALL, proportion = 0)
bttn_sizer.AddStretchSpacer(1)
bttn_sizer.Add(self.play_button, border = 5, flag = CENTER, proportion = 0)
bttn_sizer.Add(self.pause_button, border = 5, flag = CENTER, proportion = 0)
bttn_sizer.Add(self.stop_button, border = 5, flag = CENTER, proportion = 0)
app_sizer.Add(self.mpc, border = 10, flag = CENTER | wx.EXPAND, proportion = 1)
app_sizer.Add(bttn_sizer, border = 5, proportion = 1)
self.SetSizer(app_sizer)
self.Fit()
#wx.CallAfter(self.OnLoadURL, None)
# Binds
self.Bind(EVT_MEDIA_LOADED, self.OnMediaLoaded, self.mpc)
self.Bind(EVT_MEDIA_PLAY, self.OnMediaPlaying, self.mpc)
self.Bind(EVT_MEDIA_PAUSE, self.OnMediaPaused, self.mpc)
self.Bind(EVT_MEDIA_STOP, self.OnMediaStopped, self.mpc)
self.Bind(EVT_MEDIA_FINISHED, self.OnMediaFinished, self.mpc)
self.Bind(wx.EVT_BUTTON, self.OnLoadFile, self.load_file_button)
self.Bind(wx.EVT_BUTTON, self.OnLoadURL, self.load_url_button)
self.Bind(wx.EVT_BUTTON, self.OnPlay, self.play_button)
self.Bind(wx.EVT_BUTTON, self.OnPause, self.pause_button)
self.Bind(wx.EVT_BUTTON, self.OnStop, self.stop_button)
def OnLoadFile(self, event):
self.play_button.Disable()
dlg = wx.FileDialog(self, message="Choose a media file", style=wx.OPEN | wx.CHANGE_DIR )
if dlg.ShowModal() == wx.ID_OK:
path = dlg.GetPath()
if not self.mpc.Load(path):
wx.MessageBox("Unable to load %s: Unsupported format?" % path, "ERROR", wx.ICON_ERROR | wx.OK)
else:
self.mpc.SetInitialSize()
self.GetSizer().Layout()
dlg.Destroy()
def OnLoadURL(self, event):
self.play_button.Disable()
dlg = wx.TextEntryDialog(self, "Enter URL", "URL", defaultValue = "")
if dlg.ShowModal() == wx.ID_OK:
url = dlg.GetValue()
if url is not None:
self.play_button.Disable()
self.mpc.LoadURI(url)
else:
wx.MessageBox("Error: The URL you enter is invalid, Please enter a valid URL.", "Invalid URL", wx.ICON_ERROR | wx.OK)
dlg.Destroy()
def LoadURL(self, url, postdata = None):
self.mpc.LoadURI(url, postdata)
def OnPlay(self, event):
print "OnPlay"
self.mpc.Play()
def OnPause(self, event):
print "OnPause"
self.mpc.Pause()
def OnStop(self, event):
print "OnStop"
self.mpc.Stop()
def OnMediaLoaded(self, event):
print "Media Loaded"
self.play_button.Enable()
def OnMediaPlaying(self, event):
print "Playing"
def OnMediaPaused(self, event):
print "Paused"
def OnMediaStopped(self, event):
print "Stopped"
def OnMediaFinished(self, event):
print "Finished"
if __name__ == "__main__":
#from twisted.internet import wxreactor
#wxreactor.install()
#from twisted.internet import reactor
app = wx.App(False)
f = wx.Frame(None, -1, size = (400, 500), title = "Player")
f.p = Panel(f)
f.Show()
app.MainLoop()
#reactor.registerWxApp(app)
#reactor.run()