Skip to content

Commit e282fcf

Browse files
committed
camera: Report rotation needed to account for device orientation.
Fixes #11476.
1 parent 12e3162 commit e282fcf

File tree

11 files changed

+80
-13
lines changed

11 files changed

+80
-13
lines changed

include/SDL3/SDL_surface.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,12 @@ extern SDL_DECLSPEC void SDLCALL SDL_DestroySurface(SDL_Surface *surface);
241241
* left edge of the image, if this surface is being used as a cursor.
242242
* - `SDL_PROP_SURFACE_HOTSPOT_Y_NUMBER`: the hotspot pixel offset from the
243243
* top edge of the image, if this surface is being used as a cursor.
244+
* - `SDL_PROP_SURFACE_ROTATION_NUMBER`: the number of degrees a surface's
245+
* data is meant to be rotated clockwise to make the image
246+
* right-side up. Default 0. This is used by the camera API, if a mobile
247+
* device is oriented differently than what its camera provides (i.e. -
248+
* the camera always provides portrait images but the phone is being held
249+
* in landscape orientation). Since SDL 3.4.0.
244250
*
245251
* \param surface the SDL_Surface structure to query.
246252
* \returns a valid property ID on success or 0 on failure; call
@@ -257,6 +263,7 @@ extern SDL_DECLSPEC SDL_PropertiesID SDLCALL SDL_GetSurfaceProperties(SDL_Surfac
257263
#define SDL_PROP_SURFACE_TONEMAP_OPERATOR_STRING "SDL.surface.tonemap"
258264
#define SDL_PROP_SURFACE_HOTSPOT_X_NUMBER "SDL.surface.hotspot.x"
259265
#define SDL_PROP_SURFACE_HOTSPOT_Y_NUMBER "SDL.surface.hotspot.y"
266+
#define SDL_PROP_SURFACE_ROTATION_NUMBER "SDL.surface.rotation"
260267

261268
/**
262269
* Set the colorspace used by a surface.

src/camera/SDL_camera.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ static size_t GetFrameBufLen(const SDL_CameraSpec *spec)
150150
return wxh * SDL_BYTESPERPIXEL(fmt);
151151
}
152152

153-
static SDL_CameraFrameResult ZombieAcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS)
153+
static SDL_CameraFrameResult ZombieAcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS, int *rotation)
154154
{
155155
const SDL_CameraSpec *spec = &device->actual_spec;
156156

@@ -832,9 +832,10 @@ bool SDL_CameraThreadIterate(SDL_Camera *device)
832832
SDL_Surface *output_surface = NULL;
833833
SurfaceList *slist = NULL;
834834
Uint64 timestampNS = 0;
835+
int rotation = 0;
835836

836837
// AcquireFrame SHOULD NOT BLOCK, as we are holding a lock right now. Block in WaitDevice instead!
837-
const SDL_CameraFrameResult rc = device->AcquireFrame(device, device->acquire_surface, &timestampNS);
838+
const SDL_CameraFrameResult rc = device->AcquireFrame(device, device->acquire_surface, &timestampNS, &rotation);
838839

839840
if (rc == SDL_CAMERA_FRAME_READY) { // new frame acquired!
840841
#if DEBUG_CAMERA
@@ -928,6 +929,8 @@ bool SDL_CameraThreadIterate(SDL_Camera *device)
928929
acquired->pixels = NULL;
929930
acquired->pitch = 0;
930931

932+
SDL_SetNumberProperty(SDL_GetSurfaceProperties(output_surface), SDL_PROP_SURFACE_ROTATION_NUMBER, rotation);
933+
931934
// make the filled output surface available to the app.
932935
SDL_LockMutex(device->lock);
933936
slist->next = device->filled_output_surfaces.next;

src/camera/SDL_syscamera.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ struct SDL_Camera
9999

100100
// These are, initially, set from camera_driver, but we might swap them out with Zombie versions on disconnect/failure.
101101
bool (*WaitDevice)(SDL_Camera *device);
102-
SDL_CameraFrameResult (*AcquireFrame)(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS);
102+
SDL_CameraFrameResult (*AcquireFrame)(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS, int *rotation);
103103
void (*ReleaseFrame)(SDL_Camera *device, SDL_Surface *frame);
104104

105105
// All supported formats/dimensions for this device.
@@ -167,13 +167,18 @@ struct SDL_Camera
167167
struct SDL_PrivateCameraData *hidden;
168168
};
169169

170+
171+
// Note that for AcquireFrame, `rotation` is degrees, with positive values rotating clockwise. This is the amount to rotate an image so it would be right-side up.
172+
// Rotations should be in 90 degree increments at this time (landscape to portrait, or upside down to right side up, etc).
173+
// Most platforms won't care about this, but mobile devices might need to deal with the device itself being physically rotated, causing the fixed-orientation camera to be presenting sideways images.
174+
170175
typedef struct SDL_CameraDriverImpl
171176
{
172177
void (*DetectDevices)(void);
173178
bool (*OpenDevice)(SDL_Camera *device, const SDL_CameraSpec *spec);
174179
void (*CloseDevice)(SDL_Camera *device);
175180
bool (*WaitDevice)(SDL_Camera *device);
176-
SDL_CameraFrameResult (*AcquireFrame)(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS); // set frame->pixels, frame->pitch, and *timestampNS!
181+
SDL_CameraFrameResult (*AcquireFrame)(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS, int *rotation); // set frame->pixels, frame->pitch, *timestampNS, and *rotation!
177182
void (*ReleaseFrame)(SDL_Camera *device, SDL_Surface *frame); // Reclaim frame->pixels and frame->pitch!
178183
void (*FreeDeviceHandle)(SDL_Camera *device); // SDL is done with this device; free the handle from SDL_AddCamera()
179184
void (*Deinitialize)(void);

src/camera/android/SDL_camera_android.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ static bool ANDROIDCAMERA_WaitDevice(SDL_Camera *device)
295295
return true; // this isn't used atm, since we run our own thread via onImageAvailable callbacks.
296296
}
297297

298-
static SDL_CameraFrameResult ANDROIDCAMERA_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS)
298+
static SDL_CameraFrameResult ANDROIDCAMERA_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS, int *rotation)
299299
{
300300
SDL_CameraFrameResult result = SDL_CAMERA_FRAME_READY;
301301
media_status_t res;

src/camera/coremedia/SDL_camera_coremedia.m

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
#import <AVFoundation/AVFoundation.h>
3030
#import <CoreMedia/CoreMedia.h>
3131

32+
#if defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_TVOS)
33+
#import <UIKit/UIKit.h>
34+
#endif
35+
3236
/*
3337
* Need to link with:: CoreMedia CoreVideo
3438
*
@@ -77,6 +81,9 @@ @interface SDLPrivateCameraData : NSObject
7781
@property(nonatomic, retain) AVCaptureSession *session;
7882
@property(nonatomic, retain) SDLCaptureVideoDataOutputSampleBufferDelegate *delegate;
7983
@property(nonatomic, assign) CMSampleBufferRef current_sample;
84+
#if defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_TVOS)
85+
@property(nonatomic, assign) UIDeviceOrientation last_device_orientation;
86+
#endif
8087
@end
8188

8289
@implementation SDLPrivateCameraData
@@ -146,7 +153,7 @@ static bool COREMEDIA_WaitDevice(SDL_Camera *device)
146153
return true; // this isn't used atm, since we run our own thread out of Grand Central Dispatch.
147154
}
148155

149-
static SDL_CameraFrameResult COREMEDIA_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS)
156+
static SDL_CameraFrameResult COREMEDIA_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS, int *rotation)
150157
{
151158
SDL_CameraFrameResult result = SDL_CAMERA_FRAME_READY;
152159
SDLPrivateCameraData *hidden = (__bridge SDLPrivateCameraData *) device->hidden;
@@ -219,6 +226,25 @@ static SDL_CameraFrameResult COREMEDIA_AcquireFrame(SDL_Camera *device, SDL_Surf
219226

220227
CVPixelBufferUnlockBaseAddress(image, 0);
221228

229+
// As of this writing, all known iOS devices provide portrait orientation camera data, so we just need to know how to rotate between that and the current device orientation.
230+
// macOS currently assumes you don't ever rotate images because the camera is always positioned right-side up (for now).
231+
#if defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_TVOS)
232+
UIDeviceOrientation device_orientation = [[UIDevice currentDevice] orientation];
233+
if (!UIDeviceOrientationIsValidInterfaceOrientation(device_orientation)) {
234+
device_orientation = hidden.last_device_orientation; // possible the phone is laying flat or something went wrong, just stay with the last known-good orientation.
235+
} else {
236+
hidden.last_device_orientation = device_orientation; // update the last known-good orientation for later.
237+
}
238+
239+
switch (device_orientation) {
240+
case UIDeviceOrientationPortrait: *rotation = 0; break;
241+
case UIDeviceOrientationPortraitUpsideDown: *rotation = 180; break;
242+
case UIDeviceOrientationLandscapeRight: *rotation = 90; break; // !!! FIXME: might be backwards.
243+
case UIDeviceOrientationLandscapeLeft: *rotation = 270; break; // !!! FIXME: might be backwards.
244+
default: SDL_assert(!"Unexpected device orientation!"); *rotation = 0; break;
245+
}
246+
#endif
247+
222248
return result;
223249
}
224250

@@ -231,6 +257,10 @@ static void COREMEDIA_ReleaseFrame(SDL_Camera *device, SDL_Surface *frame)
231257
static void COREMEDIA_CloseDevice(SDL_Camera *device)
232258
{
233259
if (device && device->hidden) {
260+
#if defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_TVOS)
261+
[[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications];
262+
#endif
263+
234264
SDLPrivateCameraData *hidden = (SDLPrivateCameraData *) CFBridgingRelease(device->hidden);
235265
device->hidden = NULL;
236266

@@ -358,6 +388,28 @@ static bool COREMEDIA_OpenDevice(SDL_Camera *device, const SDL_CameraSpec *spec)
358388
hidden.session = session;
359389
hidden.delegate = delegate;
360390
hidden.current_sample = NULL;
391+
392+
#if defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_TVOS)
393+
// When using a camera, we turn on device orientation tracking. The docs note that this turns on
394+
// the device's accelerometer, so I assume this burns power, so we don't leave this running all
395+
// the time. These calls nest, so we just need to call the matching `end` message when we close.
396+
// You _can_ get an actual events through this mechanism, but we just want to be able to call
397+
// -[UIDevice orientation], which will update with real info while notificatons are enabled.
398+
UIDevice *uidevice = [UIDevice currentDevice];
399+
[uidevice beginGeneratingDeviceOrientationNotifications];
400+
hidden.last_device_orientation = uidevice.orientation;
401+
if (!UIDeviceOrientationIsValidInterfaceOrientation(hidden.last_device_orientation)) {
402+
// accelerometer isn't ready yet or the phone is laying flat or something. Just try to guess from how the UI is oriented at the moment.
403+
switch ([UIApplication sharedApplication].statusBarOrientation) {
404+
case UIInterfaceOrientationPortrait: hidden.last_device_orientation = UIDeviceOrientationPortrait; break;
405+
case UIInterfaceOrientationPortraitUpsideDown: hidden.last_device_orientation = UIDeviceOrientationPortraitUpsideDown; break;
406+
case UIInterfaceOrientationLandscapeLeft: hidden.last_device_orientation = UIDeviceOrientationLandscapeRight; break; // Apple docs say UI and device orientations are reversed in landscape.
407+
case UIInterfaceOrientationLandscapeRight: hidden.last_device_orientation = UIDeviceOrientationLandscapeLeft; break;
408+
default: hidden.last_device_orientation = UIDeviceOrientationPortrait; break; // oh well.
409+
}
410+
}
411+
#endif
412+
361413
device->hidden = (struct SDL_PrivateCameraData *)CFBridgingRetain(hidden);
362414

363415
[session startRunning]; // !!! FIXME: docs say this can block while camera warms up and shouldn't be done on main thread. Maybe push through `queue`?

src/camera/dummy/SDL_camera_dummy.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ static bool DUMMYCAMERA_WaitDevice(SDL_Camera *device)
3838
return SDL_Unsupported();
3939
}
4040

41-
static SDL_CameraFrameResult DUMMYCAMERA_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS)
41+
static SDL_CameraFrameResult DUMMYCAMERA_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS, int *rotation)
4242
{
4343
SDL_Unsupported();
4444
return SDL_CAMERA_FRAME_ERROR;

src/camera/emscripten/SDL_camera_emscripten.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ static bool EMSCRIPTENCAMERA_WaitDevice(SDL_Camera *device)
3939
return false;
4040
}
4141

42-
static SDL_CameraFrameResult EMSCRIPTENCAMERA_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS)
42+
static SDL_CameraFrameResult EMSCRIPTENCAMERA_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS, int *rotation)
4343
{
4444
void *rgba = SDL_malloc(device->actual_spec.width * device->actual_spec.height * 4);
4545
if (!rgba) {

src/camera/mediafoundation/SDL_camera_mediafoundation.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ static void SDLCALL CleanupIMFMediaBuffer(void *userdata, void *value)
430430
SDL_free(objs);
431431
}
432432

433-
static SDL_CameraFrameResult MEDIAFOUNDATION_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS)
433+
static SDL_CameraFrameResult MEDIAFOUNDATION_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS, int *rotation)
434434
{
435435
SDL_assert(device->hidden->current_sample != NULL);
436436

@@ -562,7 +562,7 @@ static SDL_CameraFrameResult MEDIAFOUNDATION_CopyFrame(SDL_Surface *frame, const
562562
return SDL_CAMERA_FRAME_READY;
563563
}
564564

565-
static SDL_CameraFrameResult MEDIAFOUNDATION_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS)
565+
static SDL_CameraFrameResult MEDIAFOUNDATION_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS, int *rotation)
566566
{
567567
SDL_assert(device->hidden->current_sample != NULL);
568568

src/camera/pipewire/SDL_camera_pipewire.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,7 @@ static bool PIPEWIRECAMERA_WaitDevice(SDL_Camera *device)
577577
return true;
578578
}
579579

580-
static SDL_CameraFrameResult PIPEWIRECAMERA_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS)
580+
static SDL_CameraFrameResult PIPEWIRECAMERA_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS, int *rotation)
581581
{
582582
struct pw_buffer *b;
583583

src/camera/v4l2/SDL_camera_v4l2.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ static bool V4L2_WaitDevice(SDL_Camera *device)
125125
return false;
126126
}
127127

128-
static SDL_CameraFrameResult V4L2_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS)
128+
static SDL_CameraFrameResult V4L2_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS, int *rotation)
129129
{
130130
const int fd = device->hidden->fd;
131131
const io_method io = device->hidden->io;

0 commit comments

Comments
 (0)