1
# Miro - an RSS based video player application
2
# Copyright (C) 2005-2010 Participatory Culture Foundation
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
# In addition, as a special exception, the copyright holders give
19
# permission to link the code of portions of this program with the OpenSSL
22
# You must obey the GNU General Public License in all respects for all of
23
# the code used other than OpenSSL. If you modify file(s) with this
24
# exception, you may extend this exception to your version of the file(s),
25
# but you are not obligated to do so. If you do not wish to do so, delete
26
# this exception statement from your version. If you delete this exception
27
# statement from all source files in the program, then also delete it here.
33
from objc import pathForFramework, loadBundleFunctions
34
from Foundation import *
38
from miro import prefs
39
from miro import config
40
from miro import player
41
from miro import iso639
42
from miro.plat import utils
43
from miro.plat import bundle
44
from miro.plat import qtcomp
45
from miro.plat import script_codes
46
from miro.plat.frontends.widgets import threads
47
from miro.plat.frontends.widgets.helpers import NotificationForwarder
48
from miro.util import copy_subtitle_file
50
###############################################################################
52
qt_framework = pathForFramework("QuickTime.framework")
53
qt_bundle = NSBundle.bundleWithPath_(qt_framework)
54
loadBundleFunctions(qt_bundle, globals(), (('GetMediaLanguage', 's^^{}'),))
56
###############################################################################
58
def register_components():
59
bundlePath = bundle.getBundlePath()
60
componentsDirectoryPath = os.path.join(bundlePath, 'Contents', 'Components')
61
components = glob.glob(os.path.join(componentsDirectoryPath, '*.component'))
62
for component in components:
63
cmpName = os.path.basename(component)
64
stdloc1 = os.path.join("/", "Library", "Quicktime", cmpName)
65
stdloc2 = os.path.join("/", "Library", "Audio", "Plug-Ins", "Components", cmpName)
66
if not os.path.exists(stdloc1) and not os.path.exists(stdloc2):
67
ok = qtcomp.register(component.encode('utf-8'))
69
logging.info('Successfully registered embedded component: %s' % cmpName)
71
logging.warn('Error while registering embedded component: %s' % cmpName)
73
logging.info('Skipping embedded %s registration, already installed.' % cmpName)
75
###############################################################################
77
class WarmupProgressHandler(NSObject):
79
self = super(WarmupProgressHandler, self).init()
82
def loadStateChanged_(self, notification):
83
self.handleLoadStateForMovie_(notification.object())
84
def handleInitialLoadStateForMovie_(self, movie):
85
load_state = movie.attributeForKey_(QTMovieLoadStateAttribute).longValue()
86
if load_state < QTMovieLoadStateComplete:
87
NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(
90
QTMovieLoadStateDidChangeNotification,
93
self.handleLoadStateForMovie_(movie)
94
def handleLoadStateForMovie_(self, movie):
95
load_state = movie.attributeForKey_(QTMovieLoadStateAttribute).longValue()
96
if load_state == QTMovieLoadStateComplete:
97
logging.info("QuickTime warm up complete")
98
NSNotificationCenter.defaultCenter().removeObserver_(self)
99
NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
100
10, self, 'releaseWarmupMovie:', None, False)
102
def releaseWarmupMovie_(self, timer):
103
logging.info("Releasing warmup movie.")
107
warmup_handler = WarmupProgressHandler.alloc().init()
111
logging.info('Warming up QuickTime')
112
rsrcPath = bundle.getBundleResourcePath()
114
attributes = NSMutableDictionary.dictionary()
115
attributes['QTMovieFileNameAttribute'] = os.path.join(rsrcPath, 'warmup.mov')
116
attributes['QTMovieOpenAsyncRequiredAttribute'] = True
117
attributes['QTMovieDelegateAttribute'] = None
120
warmup_movie, error = QTMovie.movieWithAttributes_error_(attributes, None)
122
if error is not None:
123
logging.warn("QuickTime Warm Up failed: %s" % error)
125
warmup_handler.handleInitialLoadStateForMovie_(warmup_movie)
127
###############################################################################
129
def qttime2secs(qttime):
130
timeScale = qttimescale(qttime)
133
timeValue = qttimevalue(qttime)
134
return timeValue / float(timeScale)
136
def qttimescale(qttime):
137
if isinstance(qttime, tuple):
140
return qttime.timeScale
142
def qttimevalue(qttime):
143
if isinstance(qttime, tuple):
146
return qttime.timeValue
148
###############################################################################
149
# The QTMediaTypeSubtitle and QTMediaTypeClosedCaption media types are only
150
# available in OS X 10.6 so we emulate them if they aren't defined.
153
dummy = QTMediaTypeSubtitle
155
QTMediaTypeSubtitle = u'sbtl'
158
dummy = QTMediaTypeClosedCaption
160
QTMediaTypeClosedCaption = u'clcp'
162
###############################################################################
164
class Player(player.Player):
166
def __init__(self, supported_media_types):
167
player.Player.__init__(self)
168
self.supported_media_types = supported_media_types
169
self.movie_notifications = None
171
self.item_info = None
174
threads.warn_if_not_on_main_thread('quicktime.Player.reset')
175
if self.movie_notifications is not None:
176
self.movie_notifications.disconnect()
177
self.movie_notifications = None
179
self.item_info = None
181
def set_item(self, item_info, callback, errback, force_subtitles=False):
182
threads.warn_if_not_on_main_thread('quicktime.Player.set_item')
183
qtmovie = self.get_movie_from_file(item_info.video_path)
185
if qtmovie is not None:
187
self.item_info = item_info
188
self.movie_notifications = NotificationForwarder.create(self.movie)
189
self.movie_notifications.connect(self.handle_movie_notification, QTMovieDidEndNotification)
190
self.setup_subtitles(force_subtitles)
195
def get_movie_from_file(self, path):
196
osfilename = utils.filename_type_to_os_filename(path)
197
url = NSURL.fileURLWithPath_(osfilename)
198
qtmovie, error = QTMovie.movieWithURL_error_(url, None)
199
if qtmovie is None or error is not None:
201
if not self.can_open_file(qtmovie):
205
def can_open_file(self, qtmovie):
206
threads.warn_if_not_on_main_thread('quicktime.Player.can_open_file')
208
duration = utils.qttimevalue(qtmovie.duration())
210
if qtmovie is not None and duration > 0:
211
allTracks = qtmovie.tracks()
212
if len(qtmovie.tracks()) > 0:
213
# Make sure we have at least one track with a non zero length
214
allMedia = [track.media() for track in allTracks]
215
for media in allMedia:
216
mediaType = media.attributeForKey_(QTMediaTypeAttribute)
217
mediaDuration = utils.qttimevalue(media.attributeForKey_(QTMediaDurationAttribute).QTTimeValue())
218
if mediaType in self.supported_media_types and mediaDuration > 0:
224
def setup_subtitles(self, force_subtitles):
225
if config.get(prefs.ENABLE_SUBTITLES) or force_subtitles:
226
enabled_tracks = self.get_all_enabled_subtitle_tracks()
227
if len(enabled_tracks) == 0:
228
tracks = self.get_subtitle_tracks()
230
self.enable_subtitle_track(tracks[0])
231
elif len(enabled_tracks) > 1:
232
track_id = enabled_tracks[-1].attributeForKey_(QTTrackIDAttribute)
233
self.disable_subtitles()
234
self.enable_subtitle_track(track_id)
236
self.disable_subtitles()
238
def get_subtitle_tracks(self):
240
if self.movie is not None:
241
for track in self.movie.tracks():
242
if self.is_subtitle_track(track):
243
media = track.media().quickTimeMedia()
244
lang = GetMediaLanguage(media)
245
display_name = track.attributeForKey_(QTTrackDisplayNameAttribute)
246
if lang == 32767: # 32764 = langUndefined
249
lang_code = script_codes.map_to_two_letters_code(lang)
250
lang_info = iso639.find(lang_code)
251
if lang_info is None:
254
name = lang_info["name"]
255
if name != display_name:
256
name = "%s (%s)" % (name, display_name)
257
is_enabled = track.attributeForKey_(QTTrackEnabledAttribute) == 1
258
track_id = track.attributeForKey_(QTTrackIDAttribute)
259
tracks.append((track_id, name, is_enabled))
262
def _find_track(self, key, value):
263
if self.movie is not None:
264
for track in self.movie.tracks():
265
if self.is_subtitle_track(track):
266
if track.attributeForKey_(key) == value:
270
def _find_all_tracks(self, key, value):
272
if self.movie is not None:
273
for track in self.movie.tracks():
274
if self.is_subtitle_track(track):
275
if track.attributeForKey_(key) == value:
279
def get_enabled_subtitle_track(self):
280
return self._find_track(QTTrackEnabledAttribute, 1)
282
def get_all_enabled_subtitle_tracks(self):
283
return self._find_all_tracks(QTTrackEnabledAttribute, 1)
285
def get_subtitle_track_by_name(self, name):
286
return self._find_track(QTTrackDisplayNameAttribute, name)
288
def get_subtitle_track_by_id(self, track_id):
289
return self._find_track(QTTrackIDAttribute, track_id)
291
def is_subtitle_track(self, track):
292
layer = track.attributeForKey_(QTTrackLayerAttribute)
293
media_type = track.attributeForKey_(QTTrackMediaTypeAttribute)
294
return (layer == -1 and media_type == QTMediaTypeVideo) or media_type in [QTMediaTypeSubtitle, QTMediaTypeClosedCaption]
296
def enable_subtitle_track(self, track_id):
297
current = self.get_enabled_subtitle_track()
298
if current is not None:
299
current.setAttribute_forKey_(0, QTTrackEnabledAttribute)
300
to_enable = self.get_subtitle_track_by_id(track_id)
301
if to_enable is not None:
302
to_enable.setAttribute_forKey_(1, QTTrackEnabledAttribute)
304
def disable_subtitles(self):
305
track = self.get_enabled_subtitle_track()
306
if track is not None:
307
track.setAttribute_forKey_(0, QTTrackEnabledAttribute)
309
def select_subtitle_file(self, sub_path, handle_successful_select):
311
handle_successful_select()
313
app.playback_manager.stop()
314
copy_subtitle_file(sub_path, self.item_info.video_path)
315
self.set_item(self.item_info, handle_ok, handle_err, True)
317
def select_subtitle_encoding(self, encoding):
318
# FIXME - set the subtitle encoding in quicktime
321
def set_volume(self, volume):
323
self.movie.setVolume_(volume)
325
def get_elapsed_playback_time(self):
326
qttime = self.movie.currentTime()
327
return utils.qttime2secs(qttime)
329
def get_total_playback_time(self):
330
if self.movie is None:
332
qttime = self.movie.duration()
333
return utils.qttime2secs(qttime)
335
def skip_forward(self):
336
current = self.get_elapsed_playback_time()
337
duration = self.get_total_playback_time()
338
pos = min(duration, current + 30.0)
339
self.seek_to(pos / duration)
341
def skip_backward(self):
342
current = self.get_elapsed_playback_time()
343
duration = self.get_total_playback_time()
344
pos = max(0, current - 15.0)
345
self.seek_to(pos / duration)
347
def seek_to(self, position):
348
qttime = self.movie.duration()
349
if isinstance(qttime, tuple):
350
qttime = (qttime[0] * position, qttime[1], qttime[2])
352
qttime.timeValue = qttime.timeValue * position
353
self.movie.setCurrentTime_(qttime)
355
def play_from_time(self, resume_time=0):
356
self.seek_to(resume_time / self.get_total_playback_time())
359
def set_playback_rate(self, rate):
360
self.movie.setRate_(rate)
362
def handle_movie_notification(self, notification):
363
if notification.name() == QTMovieDidEndNotification and not app.playback_manager.is_suspended:
364
app.playback_manager.on_movie_finished()
366
###############################################################################