Fixed import pycocoa checks.
[vlc/bindings/python.git] / examples / cocoavlc.py
1
2 # -*- coding: utf-8 -*-
3
4 # Example of using PyCocoa <http://PyPI.org/project/PyCocoa> to create
5 # a window, table and an application menu to run a video using VLC on
6 # macOS.  The Python-VLC binding <http://PyPI.Python.org/pypi/python-vlc>
7 # and the corresponding VLC App, see <http://www.VideoLan.org/index.html>.
8
9 # This VLC player has only been tested with VLC 2.2.6, 2.2.8, 3.0.1,
10 # 3.0.2 and 3.0.3 and the compatible vlc.py Python-VLC binding using
11 # 64-bit Python 2.7.14, 2.7.15, 3.6.4, 3.6.5 and/or 3.7.0 on macOS
12 # 10.13.4, 10.13.5 and 10.13.6 High Sierra.  This player does not work
13 # using PyPy Python <http://PyPy.org> nor with Intel(R) Python
14 # <http://Software.Intel.com/en-us/distribution-for-python>.
15
16 # MIT License <http://OpenSource.org/licenses/MIT>
17 #
18 # Copyright (C) 2017-2018 -- mrJean1 at Gmail dot com
19 #
20 # Permission is hereby granted, free of charge, to any person obtaining a
21 # copy of this software and associated documentation files (the "Software"),
22 # to deal in the Software without restriction, including without limitation
23 # the rights to use, copy, modify, merge, publish, distribute, sublicense,
24 # and/or sell copies of the Software, and to permit persons to whom the
25 # Software is furnished to do so, subject to the following conditions:
26 #
27 # The above copyright notice and this permission notice shall be included
28 # in all copies or substantial portions of the Software.
29 #
30 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
31 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
32 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
33 # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
34 # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
35 # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
36 # OTHER DEALINGS IN THE SOFTWARE.
37
38
39 def _PyPI(package):
40     return 'see <http://PyPI.org/project/%s>' % (package,)
41
42 try:  # PYCHOK expected
43     import vlc
44 except ImportError:
45     raise ImportError('no %s module, %s' % ('vlc.py', _PyPI('Python-VLC')))
46 try:
47     from pycocoa import __version__ as __PyCocoa__
48 except ImportError:
49     raise ImportError('no %s module, %s' % ('pycocoa', _PyPI('PyCocoa')))
50 if __PyCocoa__ < '18.08.02':
51     raise ImportError('%s %s or later required, %s' % ('pycocoa', '18.8.2',
52                      _PyPI('PyCocoa')))
53 del _PyPI
54
55 # all imports listed explicitly to help PyChecker
56 from pycocoa import App, app_title, aspect_ratio, bytes2str, closeTables, \
57                     Item, ItemSeparator, MediaWindow, Menu, OpenPanel, \
58                     printf, str2bytes, Table, z1000str, zSIstr
59 try:
60     from pycocoa import get_printer
61 except ImportError:  # printers module new in PyCocoa 18.08.04
62     get_printer = None
63
64 import os
65 import platform
66 import sys
67 from time import strftime, strptime
68 try:
69     from urllib import unquote as mrl_unquote  # Python 2
70 except ImportError:
71     from urllib.parse import unquote as mrl_unquote  # Python 3
72
73 __all__  = ('AppVLC',)
74 __version__ = '18.08.06'
75
76 _Adjust  = vlc.VideoAdjustOption  # Enum
77 # <http://Wiki.VideoLan.org/Documentation:Modules/adjust>
78 _Adjust3 = {_Adjust.Brightness: (0, 1, 2),
79             _Adjust.Contrast:   (0, 1, 2),
80             _Adjust.Gamma:   (0.01, 1, 10),
81             _Adjust.Hue:        (0, 0, 360),
82             _Adjust.Saturation: (0, 1, 3)}
83 _Argv0   = os.path.splitext(os.path.basename(__file__))[0]
84 _macOS   = platform.mac_ver()[0:3:2]  # PYCHOK false
85 _Movies  = '.m4v', '.mov', '.mp4'  # lower-case file types for movies, videos
86 _Python  = sys.version.split()[0], platform.architecture()[0]  # PYCHOK false
87 _Select  = 'Select a video file from the panel'
88 _VLC_3_  = vlc.__version__.split('.')[0] > '2' and \
89            bytes2str(vlc.libvlc_get_version().split(b'.')[0]) > '2'
90
91
92 def _mspf(fps):
93     # convert frames per second to frame length in millisecs per frame
94     return 1000.0 / (fps or 25)
95
96
97 class AppVLC(App):
98     '''The application with callback methods for C{app..._},
99        C{menu..._} and C{window..._} events.
100
101        Set things up inside the C{.__init__} and C{.appLauched_}
102        methods, start by calling the C{.run} method.
103     '''
104     adjustr   = ''
105     marquee   = None
106     logostr   = ''
107     panel     = None
108     player    = None
109     raiser    = False
110     resized   = False
111     scale     = 1  # media zoom factor
112     snapshots = 0
113     video     = None
114     window    = None
115
116     def __init__(self, video=None,       # video file name
117                        adjustr='',       # vlc.VideoAdjustOption
118                        logostr='',       # vlc.VideoLogoOption
119                        marquee=False,    # vlc.VideoMarqueeOption
120                        raiser=False,     # re-raise errors
121                        title='AppVLC'):  # window title
122         super(AppVLC, self).__init__(raiser=raiser, title=title)
123         self.adjustr = adjustr
124         self.logostr = logostr
125         self.marquee = marquee
126         self.media   = None
127         self.panel   = OpenPanel(_Select)
128         self.player  = vlc.MediaPlayer()
129         self.raiser  = raiser
130         self.video   = video
131
132     def appLaunched_(self, app):
133         super(AppVLC, self).appLaunched_(app)
134         self.window = MediaWindow(title=self.video or self.title)
135
136         if self.player:
137             # the VLC player on macOS needs an ObjC NSView
138             self.player.set_nsobject(self.window.NSview)
139             self.media = self.player.set_mrl(self.video)
140
141             if self.adjustr:  # preset video options
142                 for o in self.adjustr.lower().split(','):
143                     o, v = o.strip().split('=')
144                     o = getattr(_Adjust, o.capitalize(), None)
145                     if o is not None:
146                         self._VLCadjust(o, value=v)
147
148             if self.marquee:  # set up marquee
149                 self._VLCmarquee()
150
151             if self.logostr:  # show logo
152                 self._VLClogo(self.logostr)
153
154             menu = Menu('VLC')
155             menu.append(
156                 # the action/method name for each item
157                 # is string 'menu' + item.title + '_',
158                 # without any spaces and trailing dots,
159                 # see function pycocoa.title2action.
160                 menu.item('Open...', key='o'),
161                 menu.separator(),
162                 menu.item('Info', key='i'),
163                 menu.item('Close Windows', key='w'),
164                 menu.separator(),
165                 menu.item('Play',   key='p', ctrl=True),
166                 menu.item('Pause',  key='s', ctrl=True),
167                 menu.item('Rewind', key='r', ctrl=True),
168                 menu.separator(),
169                 menu.item('Zoom In',  key='+'),
170                 menu.item('Zoom Out', key='-'),
171                 menu.item('Faster', key='>', shift=True),
172                 menu.item('Slower', key='<', shift=True))
173             if _VLC_3_:
174                 menu.append(
175                     menu.item('Brighter'),
176                     menu.item('Darker'))
177             menu.append(
178                 menu.separator(),
179                 menu.item('Snapshot', key='s', alt=True))
180             self.append(menu)
181
182         self.menuPlay_(None)
183         self.window.front()
184         self._resize(True)
185
186     def menuBrighter_(self, item):
187         self._brightness(item, +0.1)
188
189     def menuCloseWindows_(self, item):  # PYCHOK expected
190         # close window(s) from menu Cmd+W
191         # printf('%s %r', 'close_', item)
192         if not closeTables():
193             self.terminate()
194
195     def menuDarker_(self, item):
196         self._brightness(item, -0.1)
197
198     def menuFaster_(self, item):
199         self._rate(item, 1.25)
200
201     def menuInfo_(self, item):
202         try:
203             self.menuPause_(item)
204             # display Python, vlc, libVLC, media info
205             p = self.player
206             m = p.get_media()
207
208             t = Table(' Name:bold', ' Value:200:Center:center', ' Alt:100')
209             t.append(_Argv0, __version__, '20' + __version__)
210             t.append('PyCocoa', __PyCocoa__, '20' + __PyCocoa__)
211             t.append('Python', *_Python)
212             t.append('macOS', *_macOS)
213             t.separator()
214
215             t.append('vlc.py', vlc.__version__, hex(vlc.hex_version()))
216             b = ' '.join(vlc.build_date.split()[:5])
217             t.append('built', strftime('%x', strptime(b, '%c')), vlc.build_date)
218             t.separator()
219             t.append('libVLC', bytes2str(vlc.libvlc_get_version()), hex(vlc.libvlc_hex_version()))
220             t.append('libVLC', *bytes2str(vlc.libvlc_get_compiler()).split(None, 1))
221             t.separator()
222
223             f = mrl_unquote(bytes2str(m.get_mrl()))
224             t.append('media', os.path.basename(f), f)
225             if f.lower().startswith('file://'):
226                 z = os.path.getsize(f[7:])
227                 t.append('size', z1000str(z), zSIstr(z))
228             t.append('state', str(p.get_state()))
229             f = max(p.get_position(), 0)
230             t.append('position', '%.2f%%' % (f * 100,), f)
231             f = ['%.3f s' % (ms * 1e-3) for ms in (p.get_time(), m.get_duration())]
232             t.append('time/duration', *f)  # both shown in seconds
233             t.append('track/count', z1000str(p.video_get_track()), z1000str(p.video_get_track_count()))
234             t.separator()
235
236             f = p.get_fps()
237             t.append('fps/mspf', '%.6f' % (f,), '%.3f ms' % (_mspf(f),))
238             r = p.get_rate()
239             t.append('rate', '%s%%' % (int(r * 100),), r)
240             w, h = p.video_get_size(0)
241             r = aspect_ratio(w, h) or ''
242             if r:
243                 r = '%s:%s' % r
244             t.append('video size', '%sx%s' % (w, h))  # num=0
245             t.append('aspect ratio', str(p.video_get_aspect_ratio()), r)
246             t.append('scale', '%.3f' % (p.video_get_scale(),), '%.3f' % (self.scale,))
247             t.separator()
248
249             def VLCadjustr2(option):
250                 lo, _, hi = _Adjust3[option]
251                 f = self.player.video_get_adjust_float(option)
252                 p = max(0, (f - lo)) * 100.0 / (hi - lo)
253                 t = '%.2f %.1f%%' % (f, p)
254                 return t.replace('.0%', '%').replace('.00', '.0').split()
255
256             t.append('brightness', *VLCadjustr2(_Adjust.Brightness))
257             t.append('contrast',   *VLCadjustr2(_Adjust.Contrast))
258             t.append('gamma',      *VLCadjustr2(_Adjust.Gamma))
259             t.append('hue',        *VLCadjustr2(_Adjust.Hue))
260             t.append('saturation', *VLCadjustr2(_Adjust.Saturation))
261             t.separator()
262
263             s = vlc.MediaStats()  # re-use single MediaStats instance?
264             if m.get_stats(s):
265
266                 def Kops2bpstr2(bitrate):  # convert Ko/s to bits/sec
267                     # bitrates are conventionally in kilo-octets-per-sec
268                     return zSIstr(bitrate * 8000, B='bps', K=1000).split()
269
270                 t.append('media read',     *zSIstr(s.read_bytes).split())
271                 t.append('input bitrate',  *Kops2bpstr2(s.input_bitrate))
272                 if s.input_bitrate > 0:  # XXX approximate caching, based
273                     # on <http://GitHub.com/oaubert/python-vlc/issues/61>
274                     b = s.read_bytes - s.demux_read_bytes
275                     c = b / (s.input_bitrate * 1000)
276                     t.append('input caching', '%.3f s' % (c,), zSIstr(b))
277                 t.append('demux read',     *zSIstr(s.demux_read_bytes).split())
278                 t.append('stream bitrate', *Kops2bpstr2(s.demux_bitrate))
279
280                 t.append('video decoded', z1000str(s.decoded_video),      'blocks')
281                 t.append('video played',  z1000str(s.displayed_pictures), 'frames')
282                 t.append('video lost',    z1000str(s.lost_pictures),      'frames')
283
284                 t.append('audio decoded', z1000str(s.decoded_audio),   'blocks')
285                 t.append('audio played',  z1000str(s.played_abuffers), 'buffers')
286                 t.append('audio lost',    z1000str(s.lost_abuffers),   'buffers')
287
288             t.display('Python, VLC & Media Information', width=500)
289
290         except Exception as x:
291             if self.raiser:
292                 raise
293             printf('%s', x, nl=1, nt=1)
294
295     def menuOpen_(self, item):
296         # stop the current video and show
297         # the panel to select another video
298         self.menuPause_(item)
299         self.badge.label = 'O'
300         video = self.panel.pick(_Movies)
301         if video:
302             self.window.title = self.video = video
303             self.player.set_mrl(video)
304             self.ratio = 3
305             self._resize(True)
306
307     def menuPause_(self, item):  # PYCHOK expected
308         # note, .pause() pauses and un-pauses the video,
309         # .stop() stops the video and blanks the window
310         if self.player.is_playing():
311             self.player.pause()
312             self.badge.label = 'S'
313
314     def menuPlay_(self, item_or_None):  # PYCHOK expected
315         self.player.play()
316         self.badge.label = 'P'
317
318     def menuRewind_(self, item):  # PYCHOK expected
319         self.player.set_position(0.0)
320         # can't re-play once at the end
321         # self.player.play()
322         self.badge.label = 'R'
323         self._resize(False)
324
325     def menuSlower_(self, item):
326         self._rate(item, 0.80)
327
328     def menuSnapshot_(self, item):  # PYCHOK expected
329         try:
330             w = self.lastWindow  # in PyCocoa 18.08.04+
331         except AttributeError:
332             w = self.keyWindow or self.mainWindow
333         if w:
334             self.snapshots += 1
335             s = '-'.join((_Argv0,
336                           'snapshot%d' % (self.snapshots,),
337                            w.__class__.__name__))
338             if isinstance(w, MediaWindow):
339                 self.player.video_take_snapshot(0, s + '.png', 0, 0)
340             elif get_printer:  # in PyCocoa 18.08.04+
341                 get_printer().printView(w.PMview, toPDF=s + '.pdf')
342
343     def menuZoomIn_(self, item):
344         self._zoom(item, 1.25)
345
346     def menuZoomOut_(self, item):
347         self._zoom(item, 0.80)
348
349     def windowClose_(self, window):
350         # quit or click of window close button
351         if window is self.window:
352             self.terminate()
353         super(AppVLC, self).windowClose_(window)
354
355     def windowResize_(self, window):
356         if window is self.window:
357             self._resize(False)
358         super(AppVLC, self).windowResize_(window)
359
360     def _brightness(self, unused, fraction):  # change brightness
361         self._VLCadjust(_Adjust.Brightness, fraction)
362
363     def _contrast(self, unused, fraction):  # change contrast
364         self._VLCadjust(_Adjust.Contrast, fraction)
365
366     def _gamma(self, unused, fraction):  # change gamma
367         self._VLCadjust(_Adjust.Gamma, fraction)
368
369     def _hue(self, unused, fraction):  # change hue
370         self._VLCadjust(_Adjust.Hue, fraction)
371
372     def _saturation(self, unused, fraction):  # change saturation
373         self._VLCadjust(_Adjust.Saturation, fraction)
374
375     def _rate(self, unused, factor):  # change the video rate
376         r = max(0.2, min(10.0, self.player.get_rate() * factor))
377         self.player.set_rate(r)
378
379     def _resize(self, force):  # adjust aspect ratio and marquee height
380         if force or not self.resized:
381             w, h = self.player.video_get_size()
382             # the first call returns (0, 0),
383             # subsequent calls return (w, h)
384             if h > 0 and w > 0:
385                 self.window.ratio = w, h
386                 self.resized = True
387             else:
388                 self.resized = False
389
390     def _VLCadjust(self, option, fraction=0, value=None):
391         # adjust a video option like brightness, contrast, etc.
392         p = self.player
393         # <http://Wiki.VideoLan.org/Documentation:Modules/adjust>
394         # note, .Enable must be set to 1, but once is sufficient
395         p.video_set_adjust_int(_Adjust.Enable, 1)
396         try:
397             lo, _, hi = _Adjust3[option]
398             if value is None:
399                 v = p.video_get_adjust_float(option)
400             else:
401                 v = float(value)
402             if fraction:
403                 v += fraction * (hi - lo)
404             v = float(max(lo, min(hi, v)))
405             p.video_set_adjust_float(option, v)
406         except (KeyError, ValueError):
407             pass
408
409     def _VLClogo(self, logostr):
410         # add a video logo, example "python cocoavlc.py -logo
411         # cone-altglass2.png\;cone-icon-small.png ..."
412         p = self.player
413         g = vlc.VideoLogoOption  # Enum
414         # <http://Wiki.VideoLan.org/Documentation:Modules/logo>
415         p.video_set_logo_int(g.enable, 1)
416         p.video_set_logo_int(g.position, vlc.Position.Center)
417         p.video_set_logo_int(g.opacity, 128)  # 0-255
418         # p.video_set_logo_int(g.delay, 1000)  # millisec
419         # p.video_set_logo_int(g.repeat, -1)  # forever
420         p.video_set_logo_string(g.file, logostr)
421
422     def _VLCmarquee(self, size=36):
423         # put video marquee at the bottom-center
424         p = self.player
425         m = vlc.VideoMarqueeOption  # Enum
426         # <http://Wiki.VideoLan.org/Documentation:Modules/marq/=>
427         p.video_set_marquee_int(m.Enable, 1)
428         p.video_set_marquee_int(m.Size, int(size))  # pixels
429         p.video_set_marquee_int(m.Position, vlc.Position.Bottom)
430         p.video_set_marquee_int(m.Opacity, 255)  # 0-255
431         p.video_set_marquee_int(m.Timeout, 0)  # millisec, 0==forever
432         p.video_set_marquee_int(m.Refresh, 1000)  # millisec (or sec?)
433         p.video_set_marquee_string(m.Text, str2bytes('%Y-%m-%d  %T  %z'))
434
435     def _zoom(self, unused, factor):
436         # zoom the video rate in/out
437         self.scale *= factor
438         self.player.video_set_scale(self.scale)
439
440
441 if __name__ == '__main__':  # MCCABE 20
442
443     def _Adjustr():
444         a = []  # get adjust default values
445         for n in _Adjust._enum_names_.values():
446             try:
447                 _, d, _ = _Adjust3[getattr(_Adjust, n)]
448                 a.append('%s=%s' % (n, d))
449             except KeyError:  # ignore .Enable
450                 pass
451         return ','.join(sorted(a))
452
453     _adjustr = ''
454     _argv0   = os.path.basename(sys.argv[0])  # _Title
455     _Argv0   = os.path.splitext(_argv0)[0]
456     _logostr = ''
457     _marquee = False
458     _raiser  = False
459     _timeout = None
460     _title   = os.path.splitext(_argv0)[0]
461     _video   = None
462
463     args = sys.argv[1:]
464     while args and args[0].startswith('-'):
465         o = args.pop(0)
466         t = o.lower()
467         if t in ('-h', '--help'):
468             u = ('-h|--help',
469                  '-adjust %s' % (_Adjustr(),))
470             if _VLC_3_:  # require VLC 3+ and libvlc 3+
471                 u += ('-logo <image_file_name>[\\;<image_file_name>...]',
472                       '-marquee')
473             u += ('-raiser',
474                   '-timeout <secs>',
475                   '-title <string>',
476                   '<video_file_name>')
477             printf('usage:  [%s]', ']  ['.join(u), argv0=_argv0)
478             sys.exit(0)
479         elif '-adjust'.startswith(t) and len(t) > 1 and args:
480             _adjustr = args.pop(0)
481         elif '-logo'.startswith(t) and len(t) > 1 and args and _VLC_3_:
482             _logostr = args.pop(0)
483         elif '-marquee'.startswith(t) and len(t) > 1 and _VLC_3_:
484             _marquee = True
485         elif '-raiser'.startswith(t) and len(t) > 1:
486             _raiser = True
487         elif '-timeout'.startswith(t) and len(t) > 3 and args:
488             _timeout = args.pop(0)
489         elif '-title'.startswith(t) and len(t) > 3 and args:
490             _title = args.pop(0).strip()
491         else:
492             printf('invalid option: %s', o, argv0=_argv0)
493             sys.exit(1)
494
495     if _raiser:  # get traceback at SIG- faults or ...
496         try:  # ... use: python3 -X faulthandler ...
497             import faulthandler
498             faulthandler.enable()
499         except ImportError:  # not in Python 3.3-
500             pass
501
502     if args:
503         _video = args.pop(0)
504     else:
505         printf('- %s', _Select.lower(), argv0=_argv0, nl=1, nt=1)
506         app_title(_title)  # App.title when there's no App yet
507         _video = OpenPanel('Select a video file').pick(_Movies)
508
509     if _video:
510         app = AppVLC(video=_video, adjustr=_adjustr,
511                                    logostr=_logostr,
512                                    marquee=_marquee,
513                                     raiser=_raiser,
514                                      title=_title)
515         app.run(timeout=_timeout)  # never returns