Using pyobjc and wxPython to embed QTMovieView (and QTMovie) widget into a wxPython panel and then registering needed notifications, however QTMovieDidEndNotification is not ever triggered.
Downloadable Version of Code: https://dl.dropboxusercontent.com/u/12781104/QT%20Test%20Download.zip
Notable code is in the processMovie method of the QuicktimeCtrl class where it registers the notification observers.
Crash at application closing is expected, seem to have something to do with releasing the memory at this time I am focusing on getting other issues worked out.
You need to have wxPython version 2.9 COCOA; Lower version are Carbon based whith GetHandle calls giving NSHIObjects (which I can't embed QTMovieView on, at least to my knowledge) instead of an NSView
QT Embed code wrapped up in a panel
'''
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
and simple test app (easy to replace with MediaCtrl however there are issues with it)
# 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()