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 cannot see intermediate results and therefore it is harder to check that everything goes well,
- if something goes wrong, like a sudden crash of the phone or the app becoming unresponsive, the video won’t be closed properly and the whole work might be lost.
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
- access the camera to take pictures (braindump)
- deflickering a timelapse (braindump)
- streaming video with kivy (braindump)
- timelapse (braindump)