Konubinix' site

Use the Camera With Kivy on Android

Fleeting

They are plenty of use cases involving the camera in the afterlife for my phones, so I would like to learn more about it.

At first, I assumed that streaming video through webrtc would be better and I has something that works quite well, as it involves modern and heavily used technologies. Yet, likely due to its modern aspect, it is hard to have an app that works well and deterministically across several generations of Android. Also, it’s need for signaling makes the infrastructure a bit harder that it needs to be. With kivy, however, I expect it to be easier. Using my a python runtime on android, I already created simple apps that worked the same way in androids 4 to 11, like the pomodo timer on kivy or the interval timer.

So I want to try if dealing with the camera will be that easy.

Using the high level Camera widget from kivy is straightforward, as I could experience with the simple camera with kivy app, but I want to get deeper.

analysis

kivy.uix.camera.Camera wraps (in the _camera attribute) kivy.core.camera.Camera that is extended by kivy.core.camera.camera_android.CameraAndroid

For the time being, it only wrapped the deprecated Camera api, not the camera2.

CameraAndroid provides low level functions.

It also provides the following

[...]

def grab_frame(self):
    """
    Grab current frame (thread-safe, minimal overhead)
    """
    with self._buflock:
        if self._buffer is None:
            return None
        buf = self._buffer.tostring()
        return buf

def decode_frame(self, buf):
    """
    Decode image data from grabbed frame.

    This method depends on OpenCV and NumPy - however it is only used for
    fetching the current frame as a NumPy array, and not required when
    this :class:`CameraAndroid` provider is simply used by a
    :class:`~kivy.uix.camera.Camera` widget.
    """
    import numpy as np
    from cv2 import cvtColor

    w, h = self._resolution
    arr = np.fromstring(buf, 'uint8').reshape((h + h / 2, w))
    arr = cvtColor(arr, 93)  # NV21 -> BGR
    return arr

def read_frame(self):
    """
    Grab and decode frame in one call
    """
    return self.decode_frame(self.grab_frame())


[...]

Calling read_frame will fail on the call to reshape((h + h / 2, w)) because h + h / 2 is a float and an int is expected, casting to an int helps, but I want some code that works with an old release of kivy that I won’t be able to patch.

Besides, I feel like depending on cv2 and numpy available in the phone might not be a good idea.

Therefore, I’d rather send the raw data somewhere more conventional and process the data there.

the code

Based on this analysis, we can resume the needed code to this:

from android.permissions import Permission, request_permissions

from kivy.uix.camera import Camera
import cv2
import numpy as np

request_permissions([Permission.CAMERA])
cam = Camera(resolution=(1680, 1256))._camera
cam.start()
frame = cam.grab_frame()


w, h = cam._resolution
cv2.imwrite("/tmp/test.png",
            cv2.cvtColor(
                np.fromstring(
                    frame, 'uint8
                ).reshape(
                    (int(h + h / 2),
                     w)
                ),
                cv2.COLOR_YUV2BGR_NV21)
            )

Aside from the code to compute the final image, getting a picture is pretty obvious. To know the possible resolutions to initialize the camera, I can run:

from jnius import autoclass


def list_camera_resolutions():
    Camera = autoclass('android.hardware.Camera')
    camera = Camera.open(0)
    sizes = camera.getParameters().getSupportedPictureSizes()

    if hasattr(sizes, "__iter__"):
        resolutions = [(size.width, size.height) for size in sizes]
    else: # the old version of jnius in the old poc does not provide iterators
        resolutions = []
        for i in range(sizes.size()):
            resolutions.append((
                sizes.get(i).width,
                sizes.get(i).height,
            ))
            camera.release()
    return resolutions

Note that this cannot be run in a service, or kivy will complain with

11-04 10:03:21.472 11403 11425 I poc     : [CRITICAL] [Window      ] Unable to find any valuable Window provider. Please enable debug logging (e.g. add -d if running from the command line, or change the log level in the config) and re-run your app to identify potential causes
11-04 10:03:21.472 11403 11425 I poc     : sdl2 - RuntimeError: b"Application didn't initialize properly, did you include SDL_main.h in the file containing your main() function?"
11-04 10:03:21.473 11403 11425 I poc     :   File "/app/.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/build/python-installs/poc/arm64-v8a/kivy/core/__init__.py", line 71, in core_select_lib
11-04 10:03:21.473 11403 11425 I poc     :   File "/app/.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/build/python-installs/poc/arm64-v8a/kivy/core/window/window_sdl2.py", line 165, in __init__
11-04 10:03:21.473 11403 11425 I poc     :   File "/app/.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/build/python-installs/poc/arm64-v8a/kivy/core/window/__init__.py", line 1129, in __init__
11-04 10:03:21.473 11403 11425 I poc     :   File "/app/.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/build/python-installs/poc/arm64-v8a/kivy/core/window/window_sdl2.py", line 316, in create_window
11-04 10:03:21.473 11403 11425 I poc     :   File "kivy/core/window/_window_sdl2.pyx", line 121, in kivy.core.window._window_sdl2._WindowSDL2Storage.setup_window
11-04 10:03:21.473 11403 11425 I poc     :   File "kivy/core/window/_window_sdl2.pyx", line 76, in kivy.core.window._window_sdl2._WindowSDL2Storage.die
11-04 10:03:21.473 11403 11425 I poc     :
11-04 10:03:21.473 11403 11425 I poc     : [CRITICAL] [App         ] Unable to get a Window, abort.
11-04 10:03:21.694  1884  2629 I ActivityManager: Process eu.konix.poc:service_poc (pid 11403) has died: prcp FGS

on an old phone

On my wiko cink peax 2, with python2 and an old kivy, the camera will update only when the application is not waiting for input. That means that I cannot put a rpyc endpoint and trigger manually a photo from there. Yet, the followings still works and dumps a photograph every 5 seconds.

import time

import plyer
from kivy.app import App
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.camera import Camera
from logging import getLogger

logger = getLogger(__name__)

Builder.load_string("""
<CameraClick>:
    orientation: "vertical"
    Button:
        text: "Capture"
        size_hint_y: None
        height: "48dp"
        on_press: root.capture()
""")


class CameraClick(BoxLayout):

    def __init__(self):
        super(CameraClick, self).__init__()
        self.cam = None
        Clock.schedule_interval(self.capture, 5)

    def capture(self, *args):
        if not self.cam:
            self.cam = Camera(resolution=(1680, 1256))._camera
            self.cam.start()
            plyer.vibrator.vibrate()

        frame = self.cam.grab_frame()
        if frame:
            open("/sdcard/test.txt", "w").write(frame)
        else:
            logger.debug("Got nothing")


class TestCamera(App):

    def build(self):
        return CameraClick()


def run():
    TestCamera().run()

Also, it won’t work when locking the phone and will hang on unlocking.

That is pretty promising though.

digging a bit the old phone idea: create a timelapse

phone

Let’s try to create a timelapse. To preserve the battery, I added code to keep the the screen on, but very dimmed, as I remarked that the camera won’t work properly with a screen shutdown.

To help preserving the battery even more, I make use of a thread that wakes up the screen before grabbing a frame. I cannot use the kivy clock to do so for it freezes when the screen is off.

import time

import plyer
from kivy.app import App
from jnius import autoclass
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.core.camera import Camera
import os
from datetime import datetime
from kivy.uix.screenmanager import Screen, ScreenManagerException, ScreenManager
import threading
from android import mActivity
from contextlib import contextmanager
import requests
from logging import getLogger

from helpers.osc import to_service, oschandler
from helpers.wakelock import WakeLock

logger = getLogger(__name__)

wl = WakeLock('timelapse', wakeup=True)

Builder.load_string("""
<Timelapse>:
    orientation: "vertical"

    Button:
        text: "Clean"
        size_hint_y: None
        height: "48dp"
        on_press: root.clean()

    BoxLayout:
        orientation: "horizontal"

        Label:
            text: "Front camera"

        CheckBox:
            id: front

    Spinner:
        id: resolution
        values: "(1600, 1200)"
        text: "(1600, 1200)"

    BoxLayout:
        orientation: "horizontal"
        Label:
            text: "period"

        TextInput:
            # setting the id of the widget
            id: period
            text: '0.5'

    Spinner:
        id: action
        values: "upload", "save"
        text: "save"

    Label:
        id: message
        text: 'Press start to begin'

    Button:
        id: toggle
        text: "Start"
        size_hint_y: None
        height: "48dp"
        on_press: root.toggle()
""")

class TimelapseScreen(Screen):
    def __init__(self, *args, **kwargs):
        super(TimelapseScreen, self).__init__(*args, **kwargs)
        self.timelapse = Timelapse()
        self.add_widget(self.timelapse)
        self.bind(on_pre_leave=self.pre_leave_handler)
        self.bind(on_pre_enter=self.pre_enter_handler)

    def pre_leave_handler(self, *args):
        self.timelapse.stop()

    def pre_enter_handler(self, *args):
        self.timelapse.pre_enter()

class Timelapse(BoxLayout):

    def __init__(self):
        super(Timelapse, self).__init__()
        self.camera_dir = "/sdcard/camera"
        self.cam = None
        self.capture_thread = None
        self.running = False
        self.ids.front.bind(active=self.on_front_change)

        @oschandler("timelapse:start")
        def _(config):
            import json
            config = json.loads(config)
            if self.running:
                self.toggle()

            self.clean()

            self.ids.period.text = str(config["period"])
            self.toggle()

        @oschandler("timelapse:stop")
        def _(config):
            if self.running:
                self.toggle()

    def pre_enter(self):
        to_service("timelapse:enter")

    def on_front_change(self, *a):
        self.ids.resolution.values = [str(resolution) for resolution in self.list_camera_resolutions()]
        if self.ids.resolution.text not in self.ids.resolution.values:
            self.ids.resolution.text = self.ids.resolution.values[0]

    def list_camera_resolutions(self):
        Camera = autoclass('android.hardware.Camera')
        camera = Camera.open(1 if self.ids.front.active else 0)
        sizes = camera.getParameters().getSupportedPictureSizes()

        if hasattr(sizes, "__iter__"):
            resolutions = [(size.width, size.height) for size in sizes]
        else: # the old version of jnius in the old poc does not provide iterators
            resolutions = []
            for i in range(sizes.size()):
                resolutions.append((
                    sizes.get(i).width,
                    sizes.get(i).height,
                ))

        camera.release()
        return resolutions

    def stop(self):
        self.running = False
        if self.capture_thread is not None:
            self.capture_thread.join()
            self.capture_thread = None

        self.brightness(1)
        wl.release()
        if self.cam:
            self.cam._release_camera()
            self.cam = None

    def brightness(self, value):
        from jnius import autoclass
        from android.runnable import run_on_ui_thread
        WindowManagerLayoutParams = autoclass('android.view.WindowManager$LayoutParams')
        window = mActivity.getWindow()
        layout_params = window.getAttributes()
        layout_params.screenBrightness = value
        @run_on_ui_thread
        def do():
            window.setAttributes(layout_params)

        do()

    def start(self, *args):
        with wl.awake():
            self.brightness(0.01)
            logger.debug("Here we go")
            if not self.cam:
                logger.debug("Initializing Camera")
                self.cam = Camera(
                    resolution=eval(self.ids.resolution.text),
                    index=1 if self.ids.front.active else 0,
                )
                self.cam.start()
                # trying to compensate for the video becoming dark in low light environements
                try:
                    params = self.cam._android_camera.getParameters()
                    params.setExposureCompensation(params.getMaxExposureCompensation())
                    if params.isAutoExposureLockSupported():
                        params.setAutoExposureLock(False)
                    self.cam._android_camera.setParameters(params)
                except Exception as e:
                    logger.warning(e.msg)
                plyer.vibrator.vibrate()

        self.running = True
        self.capture_thread = threading.Thread(target=self.capture_routine)
        self.capture_thread.start()

    def toggle(self, *args):
        if self.running:
            self.stop()
            self.ids.toggle.text = "Start"
        else:
            self.start()
            self.ids.toggle.text = "Stop"

    def need_screen_off(self):
        return float(self.ids.period.text) > 120.

    def capture_routine(self, *args):
        import time
        while self.running:
            wl.acquire()
            if self.need_screen_off():
                # let some time for the camera to adjust
                time.sleep(3)
            logger.debug("Grabbing a frame")
            frame = self.cam.grab_frame()
            if frame:
                logger.debug("{}: Frame captured".format(datetime.now()))
                filename = self.generate_filename()
                message = getattr(self, self.ids.action.text)(frame, filename)
                self.ids.message.text = "{}: {} ({})".format(self.ids.action.text, filename, message)
            else:
                logger.debug("{}: No frame captured".format(datetime.now()))

            if self.need_screen_off():
                wl.release()

            time.sleep(float(self.ids.period.text))

    def generate_filename(self):
        now = datetime.now()
        formatted_time = now.strftime("%Y%m%d_%H%M%S") + ".{:06d}".format(now.microsecond)
        filename = "{}_{}x{}.bin".format(formatted_time, *eval(self.ids.resolution.text))
        return filename

    def save(self, frame, filename):
        if not os.path.exists(self.camera_dir):
            os.mkdir(self.camera_dir)

        open(os.path.join(self.camera_dir, filename), "wb").write(frame)
        return ""

    def clean(self):
        import shutil
        if os.path.exists(self.camera_dir):
            shutil.rmtree(self.camera_dir)

    def upload(self, frame, filename):
        try:
            response = requests.post(
                "http://192.168.1.245:9999/upload?filename={}".format(filename),
                headers={'Content-Type': 'application/octet-stream'},
                data=frame,
            )
            message = ""
            logger.debug("Upload successful: ", response.text)
        except Exception as e:
            message = "upload failed"
            logger.debug("Upload failed: {}".format(e))
        return message


class TimelapseApp(App):

    @staticmethod
    def populate(sm):
        try:
            sm.get_screen('timelapse')
            return
        except ScreenManagerException:
            pass # not populated yet

        sm.add_widget(TimelapseScreen(name='timelapse'))

    def build(self):
        sm = ScreenManager()
        self.populate(sm)
        return sm


def run():
    TimelapseApp().run()

server

Then, run locally a server to get the uploaded content, fortunately, I already have a docker image to upload data.

docker run -ti -e DIR=/upload/ -e PORT=9999 -p 9999:9999 -v $(pwd):/upload konubinix/uploader:0.1.1

Then post process all the files, with some code like this

import os
from pathlib import Path

import cv2
import numpy as np


def process_bin_files(directory):
    bin_files = Path(directory).glob('*.bin')

    for bin_file in bin_files:
        with bin_file.open('rb') as f:
            try:
                base_name = bin_file.stem
                _, dimensions = base_name.rsplit('_', 1)
                w, h = map(int, dimensions.split('x'))
            except ValueError:
                print(
                    f"Error parsing dimensions from filename: {bin_file.name}")
                continue

            png_filename = bin_file.with_suffix('.png')
            print(bin_file)
            cv2.imwrite(
                str(png_filename),
                cv2.cvtColor(
                    np.frombuffer(f.read(), dtype='uint8').reshape(
                        (int(h + h / 2), w)), cv2.COLOR_YUV2BGR_NV21))

        print(f"Processed {bin_file.name} and saved as {png_filename.name}")

        bin_file.unlink()
        print(f"Deleted original file: {bin_file.name}")


if __name__ == "__main__":
    process_bin_files(".")

Let’s call that file parser. You simply need to install opencv and run it

python3 -m venv venv
source ./venv/bin/activate
python3 -m pip install opencv-python
python3 parser.py

Now, let’s get even deeper and create a video from those images. (timelapse)

import cv2
import os
from pathlib import Path

def create_video_from_images(image_folder, output_file, fps):
    image_files = sorted(Path(image_folder).glob('*.png'))
    if not image_files:
        print("No PNG files found in the specified folder.")
        return

    # Read the first image to get the width and height
    first_image = cv2.imread(str(image_files[0]))
    height, width, layers = first_image.shape

    fourcc = cv2.VideoWriter_fourcc(*'mp4v')
    video = cv2.VideoWriter(output_file, fourcc, fps, (width, height))

    for image_file in image_files:
        img = cv2.imread(str(image_file))
        img = cv2.rotate(img, cv2.ROTATE_180)
        video.write(img)

    video.release()
    print(f"Video created successfully: {output_file}")

if __name__ == "__main__":
    image_folder = "."
    output_file = "output_video.mp4"
    fps = 5

    create_video_from_images(image_folder, output_file, fps)

note about CamcorderProfile

After working on this, I also discovered that using the CamcorderProfile, one already had access to profiles dedicated to timelapse such as QUALITY_TIME_LAPSE_480P.

Let’s try editing out code to use it instead.

import time

import plyer
from kivy.app import App
from jnius import autoclass
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.core.camera import Camera
import os
from datetime import datetime
from kivy.uix.screenmanager import Screen, ScreenManagerException, ScreenManager
import threading
from android import mActivity
from contextlib import contextmanager
from logging import getLogger

from helpers.osc import to_service, oschandler
from helpers.wakelock import WakeLock
MediaRecorder = autoclass('android.media.MediaRecorder')
AudioSource = autoclass('android.media.MediaRecorder$AudioSource')
VideoSource = autoclass('android.media.MediaRecorder$VideoSource')
OutputFormat = autoclass('android.media.MediaRecorder$OutputFormat')
AudioEncoder = autoclass('android.media.MediaRecorder$AudioEncoder')
VideoEncoder = autoclass('android.media.MediaRecorder$VideoEncoder')
CamcorderProfile = autoclass('android.media.CamcorderProfile')

logger = getLogger(__name__)
wl = WakeLock('timelapse', wakeup=True)

Builder.load_string("""
<Timelapse>:
    orientation: "vertical"

    BoxLayout:
        orientation: "horizontal"
        Label:
            text: "period"

        TextInput:
            # setting the id of the widget
            id: period
            text: '0.5'

    Label:
        id: message
        text: 'Press start to begin'

    Button:
        id: toggle
        text: "Start"
        size_hint_y: None
        height: "48dp"
        on_press: root.toggle()
""")

class TimelapseScreen(Screen):
    def __init__(self, *args, **kwargs):
        super(TimelapseScreen, self).__init__(*args, **kwargs)
        self.timelapse = Timelapse()
        self.add_widget(self.timelapse)
        self.bind(on_pre_leave=self.pre_leave_handler)

    def pre_leave_handler(self, *args):
        self.timelapse.stop()

class Timelapse(BoxLayout):

    def __init__(self):
        super(Timelapse, self).__init__()
        self.result = "/sdcard/timelapse.mp4"
        self.cam = None
        self.recorder = None

        @oschandler("timelapse:start")
        def _(config):
            import json
            config = json.loads(config)
            if self.running:
                self.toggle()

            self.clean()

            self.ids.period.text = str(config["period"])
            self.toggle()

        @oschandler("timelapse:stop")
        def _(config):
            if self.running:
                self.toggle()

    @property
    def running(self):
        return self.recorder is not None

    def stop(self):
        self.recorder.stop()
        self.recorder.release()
        self.cam._release_camera()
        self.recorder = None
        self.cam = None
        wl.release()

    def brightness(self, value):
        from jnius import autoclass
        from android.runnable import run_on_ui_thread
        WindowManagerLayoutParams = autoclass('android.view.WindowManager$LayoutParams')
        window = mActivity.getWindow()
        layout_params = window.getAttributes()
        layout_params.screenBrightness = value
        @run_on_ui_thread
        def do():
            window.setAttributes(layout_params)

        do()

    def start(self, *args):
        wl.acquire()
        self.brightness(0.01)
        logger.debug("Initializing Camera")
        self.cam = Camera(
            resolution=(1600, 1200), # that does not matter I suppose
            index=0, # index=1 for front camera (in general)
        )
        self.cam._android_camera.unlock()
        plyer.vibrator.vibrate()

        self.recorder = MediaRecorder()

        self.recorder.setCamera(self.cam._android_camera)
        self.recorder.setVideoSource(VideoSource.CAMERA)

        profile = CamcorderProfile.get(CamcorderProfile.QUALITY_TIME_LAPSE_480P)
        self.recorder.setProfile(profile)

        self.recorder.setCaptureRate(1. / float(self.ids.period.text))

        self.recorder.setOutputFile(self.result)

        self.recorder.prepare()
        self.recorder.start()

    def toggle(self, *args):
        if self.recorder:
            self.stop()
            self.ids.toggle.text = "Start"
        else:
            self.start()
            self.ids.toggle.text = "Stop"

    def clean(self):
        import shutil
        if os.path.exists(self.result):
            os.unlink(self.result)

class TimelapseApp(App):

    @staticmethod
    def populate(sm):
        try:
            sm.get_screen('timelapse')
            return
        except ScreenManagerException:
            pass # not populated yet

        sm.add_widget(TimelapseScreen(name='timelapse'))

    def build(self):
        sm = ScreenManager()
        self.populate(sm)
        return sm


def run():
    TimelapseApp().run()

let’s put it to a test

I performed the test with the wiko cink peax 2.

period (s) release lock release camera video result
60 yes no no https://konubinix.eu/ipfs/bafybeiapplwrur3c7kyz3yaugaj2j7f2mxyphkdpfbtuux7jnhpe3exfaq?filename=output_video.mp4
20 no no no https://konubinix.eu/ipfs/bafybeibpzjxzyyfdsczriqdb2jsqae5tekium3zhrk67apabplys4it43y?filename=output_video.mp4
20 no no yes https://konubinix.eu/ipfs/bafybeierkttbuki2f5hfm5tnk6ks72mp6chblwcp2m55b7q74mt6uc73ky?filename=a.mp4

They all went quite well for the 12h time of the experiments, without much impact on the battery.

The experiment with the CamcorderProfile has 3 cuts because I got the phone back to check the intermediate result. This was not easy to do remotely.

I looks like the test with releasing the wakelock led to photos of lesser quality. I assume that the camera tries to focus and adjust the brightness when the screen is awaken up and that the grab_frame that follows may grab a frame before this process has stabilized. But I won’t dig into this, because keeping the screen awake it more than enough for the timelapses I have in mind.

Also, using the CamcorderProfile and MediaRecorder led to the best results, but with the drawbacks that:

I could try to mitigate those, by streaming the content (like in spydroid) and improve the remote connection, but that is a story for another time.

recording a video

Now that we understood bit MediaRecorder, this is quite easy:

import time

import plyer
from kivy.app import App
from jnius import autoclass
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.core.camera import Camera
import os
from datetime import datetime
from kivy.uix.screenmanager import Screen, ScreenManagerException, ScreenManager
import threading
from android import mActivity
from contextlib import contextmanager
from logging import getLogger

from helpers.osc import to_service, oschandler
from helpers.wakelock import WakeLock
MediaRecorder = autoclass('android.media.MediaRecorder')
AudioSource = autoclass('android.media.MediaRecorder$AudioSource')
VideoSource = autoclass('android.media.MediaRecorder$VideoSource')
OutputFormat = autoclass('android.media.MediaRecorder$OutputFormat')
AudioEncoder = autoclass('android.media.MediaRecorder$AudioEncoder')
VideoEncoder = autoclass('android.media.MediaRecorder$VideoEncoder')
CamcorderProfile = autoclass('android.media.CamcorderProfile')

logger = getLogger(__name__)
wl = WakeLock('videorecorder', wakeup=True)

Builder.load_string("""
<Videorecorder>:
    orientation: "vertical"

    Label:
        id: message
        text: 'Press start to begin'

    Button:
        id: toggle
        text: "Start"
        on_press: root.toggle()

    Button:
        size_hint_y: 0.5
        text: "Clean"
        on_press: root.clean()

""")

class VideorecorderScreen(Screen):
    def __init__(self, *args, **kwargs):
        super(VideorecorderScreen, self).__init__(*args, **kwargs)
        self.videorecorder = Videorecorder()
        self.add_widget(self.videorecorder)
        self.bind(on_pre_leave=self.pre_leave_handler)

    def pre_leave_handler(self, *args):
        self.videorecorder.stop()

class Videorecorder(BoxLayout):

    def __init__(self):
        super(Videorecorder, self).__init__()
        self.cam = None
        self.recorder = None
        self.video_dir = "/sdcard/videos"

        @oschandler("videorecorder:start")
        def _(config):
            if self.running:
                self.toggle()

            self.toggle()

        @oschandler("videorecorder:stop")
        def _(config):
            if self.running:
                self.toggle()

    @property
    def running(self):
        return self.recorder is not None

    def stop(self):
        self.brightness(1)
        self.recorder.stop()
        self.recorder.release()
        self.cam._release_camera()
        self.recorder = None
        self.cam = None
        wl.release()
        # toggle button to its original background
        self.ids.toggle.background_color = (0, 1, 0, 1)

    def brightness(self, value):
        from jnius import autoclass
        from android.runnable import run_on_ui_thread
        WindowManagerLayoutParams = autoclass('android.view.WindowManager$LayoutParams')
        window = mActivity.getWindow()
        layout_params = window.getAttributes()
        layout_params.screenBrightness = value
        @run_on_ui_thread
        def do():
            window.setAttributes(layout_params)

        do()

    def start(self, *args):
        wl.acquire()
        self.brightness(0.01)
        logger.debug("Initializing Camera")
        self.cam = Camera(
            resolution=(1600, 1200), # that does not matter I suppose
            index=0, # index=1 for front camera (in general)
        )
        self.cam._android_camera.unlock()
        self.ids.toggle.background_color = (1, 0, 1, 1)

        self.recorder = MediaRecorder()

        self.recorder.setCamera(self.cam._android_camera)
        self.recorder.setVideoSource(VideoSource.CAMERA)
        self.recorder.setAudioSource(AudioSource.MIC)

        # profile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH)
        # self.recorder.setProfile(profile)
        self.recorder.setOutputFormat(OutputFormat.DEFAULT)
        self.recorder.setAudioEncoder(AudioEncoder.DEFAULT)
        self.recorder.setVideoEncoder(VideoEncoder.DEFAULT)
        self.recorder.setVideoSize(1600, 1200)

        self.recorder.setOutputFile(self.generate_filename())

        self.recorder.prepare()
        self.recorder.start()

        # trying to compensate for the video becoming dark in low light environements
        params = self.cam._android_camera.getParameters()
        params.setExposureCompensation(params.getMaxExposureCompensation())
        if params.isAutoExposureLockSupported():
            params.setAutoExposureLock(False)
        self.cam._android_camera.setParameters(params)


    def generate_filename(self):
        if not os.path.exists(self.video_dir):
            os.mkdir(self.video_dir)
        now = datetime.now()
        formatted_time = now.strftime("%Y%m%d_%H%M%S") + ".{:06d}".format(now.microsecond)
        filename = os.path.join(self.video_dir, "{}.mp4".format(formatted_time))
        return filename

    def toggle(self, *args):
        if self.recorder:
            self.stop()
            self.ids.toggle.text = "Start"
        else:
            self.start()
            self.ids.toggle.text = "Stop"

    def clean(self):
        import shutil
        if os.path.exists(self.video_dir):
            os.unlink(self.video_dir)

class VideorecorderApp(App):

    @staticmethod
    def populate(sm):
        try:
            sm.get_screen('videorecorder')
            return
        except ScreenManagerException:
            pass # not populated yet

        sm.add_widget(VideorecorderScreen(name='videorecorder'))

    def build(self):
        sm = ScreenManager()
        self.populate(sm)
        return sm


def run():
    VideorecorderApp().run()

Notes linking here