Last active
August 16, 2022 16:05
-
-
Save ivlevdenis/02d1017ccf860276ab7d to your computer and use it in GitHub Desktop.
kivy android native videoview
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
import re | |
from kivy.logger import Logger | |
from kivy.properties import (ObjectProperty, BooleanProperty, StringProperty, | |
OptionProperty, NumericProperty, ReferenceListProperty) | |
from kivy.uix.widget import Widget | |
from kivy.utils import platform | |
from jnius import PythonJavaClass, autoclass, java_method, cast | |
if platform == 'android': | |
from android.runnable import run_on_ui_thread | |
VideoView = autoclass('android.widget.VideoView') | |
URI = autoclass('android.net.Uri') | |
Gravity = autoclass('android.view.Gravity') | |
LinearLayout = autoclass('android.widget.LinearLayout') | |
LayoutParams = autoclass('android.widget.LinearLayout$LayoutParams') | |
activity = autoclass('org.renpy.android.PythonActivity').mActivity | |
class VideoPreparedCallback(PythonJavaClass): | |
# http://developer.android.com/reference/android/media/MediaPlayer.OnPreparedListener.html | |
__javainterfaces__ = ('android.media.MediaPlayer$OnPreparedListener', ) | |
def __init__(self, callback): | |
super(VideoPreparedCallback, self).__init__() | |
self.callback = callback | |
@java_method('(Landroid/media/MediaPlayer;)V') | |
def onPrepared(self, mp): | |
self.callback(mp) | |
# class VideoInfoCallback(PythonJavaClass): | |
# http://developer.android.com/reference/android/media/MediaPlayer.OnInfoListener.html | |
# __javainterfaces__ = ('android.media.MediaPlayer$OnInfoListener', ) | |
# def __init__(self, callback): | |
# super(VideoInfoCallback, self).__init__() | |
# self.callback = callback | |
# @java_method('(Landroid/media/MediaPlayer;II)B') | |
# def onInfo(self, mp, what, extra): | |
# """True if the method handled the info, false if it didn't. | |
# Returning false, or not having an OnErrorListener at all, | |
# will cause the info to be discarded.""" | |
# self.callback(mp, what, extra) | |
class VideoErrorCallback(PythonJavaClass): | |
# http://developer.android.com/reference/android/media/MediaPlayer.OnErrorListener.html | |
__javainterfaces__ = ('android.media.MediaPlayer$OnErrorListener', ) | |
def __init__(self, callback): | |
super(VideoErrorCallback, self).__init__() | |
self.callback = callback | |
@java_method('(Landroid/media/MediaPlayer;II)B') | |
def onError(self, mp, what, extra): | |
"""True if the method handled the error, false if it didn't. | |
Returning false, or not having an OnErrorListener at all, | |
will cause the OnCompletionListener to be called.""" | |
self.callback(mp, what, extra) | |
class VideoCompletionCallback(PythonJavaClass): | |
# http://developer.android.com/reference/android/media/MediaPlayer.OnCompletionListener.html | |
__javainterfaces__ = ('android.media.MediaPlayer$OnCompletionListener', ) | |
def __init__(self, callback): | |
super(VideoCompletionCallback, self).__init__() | |
self.callback = callback | |
@java_method('(Landroid/media/MediaPlayer;)V') | |
def onCompletion(self, mp): | |
self.callback(mp) | |
class AndroidWidgetHolder(Widget): | |
'''Act as a placeholder for an Android widget. | |
It will automatically add / remove the android view depending if the widget | |
view is set or not. The android view will act as an overlay, | |
so any graphics instruction in this area will be covered by the overlay. | |
''' | |
view = ObjectProperty(allownone=True) | |
'''Must be an Android View | |
''' | |
def __init__(self, **kwargs): | |
self._old_view = None | |
from kivy.core.window import Window | |
self._window = Window | |
kwargs['size_hint'] = (None, None) | |
super(AndroidWidgetHolder, self).__init__(**kwargs) | |
def on_view(self, instance, view): | |
self._on_view(instance, view) | |
@run_on_ui_thread | |
def _on_view(self, instance, view): | |
if self._old_view is not None: | |
layout = cast(LinearLayout, self._old_view.getParent()) | |
layout.removeView(self._old_view) | |
self._old_view = None | |
if view is None: | |
return | |
# activity = PythonActivity.mActivity | |
lp = LayoutParams(*self.size) | |
lp.gravity = Gravity.CENTER | |
activity.addContentView(view, lp) | |
view.setZOrderOnTop(True) | |
view.setX(self.x) | |
view.setY(self._window.height - self.y - self.height) | |
self._old_view = view | |
def on_size(self, instance, size): | |
self._on_size(instance, size) | |
@run_on_ui_thread | |
def _on_size(self, instance, size): | |
if self.view: | |
Logger.info('AndroidWidgetHolder: Set size to {0}'.format(size)) | |
params = self.view.getLayoutParams() | |
params.width = self.width | |
params.height = self.height | |
params.gravity = Gravity.CENTER | |
lp = LayoutParams(*self.size) | |
lp.gravity = Gravity.CENTER | |
self.view.setLayoutParams(lp) | |
# self.view.setX(self.x + self.width) | |
self.view.setY(self._window.height - self.y - self.height) | |
v = self.view | |
self.view = None | |
self.view = v | |
def on_x(self, instance, x): | |
self._on_x(instance, x) | |
@run_on_ui_thread | |
def _on_x(self, instance, x): | |
if self.view: | |
self.view.setX(x) | |
def on_y(self, instance, y): | |
self._on_y(instance, y) | |
@run_on_ui_thread | |
def _on_y(self, instance, y): | |
if self.view: | |
self.view.setY(self._window.height - self.y - self.height) | |
class AndroidVideoView(Widget): | |
norm_image_width = NumericProperty(0) | |
norm_image_height = NumericProperty(0) | |
norm_image_size = ReferenceListProperty( | |
norm_image_width, norm_image_height) | |
autoplay = BooleanProperty(False) | |
duration = NumericProperty(-1) | |
eos = BooleanProperty(False) | |
filename = StringProperty('') | |
loaded = BooleanProperty(False) | |
options = ObjectProperty({}) | |
position = NumericProperty(-1) | |
state = OptionProperty('stop', options=['play', 'pause', 'stop']) | |
def __init__(self, **kwargs): | |
self._holder = None | |
self.videoview = None | |
super(AndroidVideoView, self).__init__(**kwargs) | |
self._holder = AndroidWidgetHolder(size=self.size, pos=self.pos) | |
self.add_widget(self._holder) | |
self.create_videoview() | |
@run_on_ui_thread | |
def create_videoview(self, *args): | |
self.videoview = VideoView(activity) | |
Logger.info('AndroidVideoView: Create new VideoView widget') | |
self._holder.view = self.videoview | |
self._prepared_callback = VideoPreparedCallback(self._on_prepared) | |
self.videoview.setOnPreparedListener(self._prepared_callback) | |
# self._info_callback = VideoInfoCallback(self._on_info) | |
# self.videoview.setOnInfoListener(self._info_callback) | |
self._error_callback = VideoErrorCallback(self._on_error) | |
self.videoview.setOnErrorListener(self._error_callback) | |
self._complete_callback = VideoCompletionCallback(self._on_completion) | |
self.videoview.setOnCompletionListener(self._complete_callback) | |
def _on_prepared(self, mp): | |
self.norm_image_width = mp.getVideoWidth() | |
self.norm_image_height = mp.getVideoHeight() | |
self.loaded = True | |
# def _on_info(self, mp, what, extra): | |
# pass | |
def _on_error(self, mp, what, extra): | |
self.loaded = False | |
self.norm_image_width = 0 | |
self.norm_image_height = 0 | |
def _on_completion(self, mp): | |
self.state = 'stop' | |
self.eos = True | |
def on_autoplay(self, instance, value): | |
if self.videoview is not None and self.loaded and value: | |
self.videoview.start() | |
Logger.info('AndroidVideoView: Set autoplay to {0}'.format(value)) | |
def on_filename(self, instance, value): | |
self._on_filename(instance, value) | |
@run_on_ui_thread | |
def _on_filename(self, instance, value): | |
if re.match('(?:http|ftp|https|file)://', value) is not None: | |
fn = URI.parse(value) | |
self.videoview.setVideoURI(fn) | |
elif os.path.isfile(value): | |
self.videoview.setVideoPath(value) | |
else: | |
Logger.error('AndroidVideoView: File not found or \ | |
not supported URI schema "{0}"'.format(value)) | |
return | |
Logger.info('AndroidVideoView: Set filename to {0}'.format(value)) | |
def on_loaded(self, instance, value): | |
self._on_loaded(instance, value) | |
@run_on_ui_thread | |
def _on_loaded(self, instance, value): | |
if value: | |
self.pos = (self.x + (self.width - self.norm_image_width)/2, self.pos[1]) | |
self.duration = self.videoview.getDuration() | |
if self.autoplay: | |
self.state = 'play' | |
if self.state == 'play' and not self.videoview.isPlaying(): | |
self.videoview.start() | |
Logger.info('AndroidVideoView: Set loaded to {0}'.format(value)) | |
# def _on_position(self, dt): | |
# if self.videoview.isPlaying(): | |
# self.position = self.videoview.getDuration() | |
# def on_position(self, instance, value): | |
# if value < 0: | |
# return | |
# Clock.schedule_once(self._on_position, 0.5) | |
def on_state(self, instance, value): | |
self._on_state(instance, value) | |
@run_on_ui_thread | |
def _on_state(self, instance, value): | |
if value == 'play': | |
if self.loaded: | |
self.videoview.start() | |
self.eos = False | |
self.position = 0 | |
elif value == 'pause': | |
if self.state == 'play' and self.videoview.isPlaying(): | |
self.videoview.pause() | |
elif value == 'stop': | |
if self.videoview.isPlaying(): | |
self.videoview.stopPlayback() | |
self.eos = True | |
self.position = -1 | |
Logger.info('AndroidVideoView: Set state to {0}'.format(value)) | |
def on_size(self, instance, size): | |
self._on_size(instance, size) | |
@run_on_ui_thread | |
def _on_size(self, instance, size): | |
if self._holder: | |
self._holder.size = size | |
Logger.info('AndroidVideoView: Set size to {0}'.format(size)) | |
def on_pos(self, instance, pos): | |
self._on_pos(instance, pos) | |
@run_on_ui_thread | |
def _on_pos(self, instance, pos): | |
if self._holder: | |
self._holder.pos = pos | |
Logger.info('AndroidVideoView: Set pos to {0}'.format(pos)) | |
# @run_on_ui_thread | |
# def _on_norm_image_size(self, instance, norm_image_size): | |
# def on_norm_image_size(self, instance, norm_image_size): | |
# self.size = norm_image_size |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment