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