使用 pyobjc 和 wxPython 将 QTMovieView(和 QTMovie)小部件嵌入到 wxPython 面板中,然后注册所需的通知,但是不会触发 QTMovieDidEndNotification。
可下载的代码版本:https ://dl.dropboxusercontent.com/u/12781104/QT%20Test%20Download.zip
值得注意的代码在 QuicktimeCtrl 类的 processMovie 方法中,它注册了通知观察者。
预计应用程序关闭时崩溃,似乎与释放内存有关,此时我正专注于解决其他问题。
你需要有 wxPython 2.9 版本的 COCOA;较低的版本是基于 Carbon 的 GetHandle 调用提供 NSHIObjects(至少据我所知,我无法在其上嵌入 QTMovieView)而不是 NSView
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()