{"id":187,"date":"2026-01-18T17:05:36","date_gmt":"2026-01-18T17:05:36","guid":{"rendered":"https:\/\/www.visitnyc.store\/blog\/?p=187"},"modified":"2026-01-18T17:06:52","modified_gmt":"2026-01-18T17:06:52","slug":"camera-permission-side-by-side-ffmpeg","status":"publish","type":"post","link":"https:\/\/www.visitnyc.store\/blog\/camera-permission-side-by-side-ffmpeg\/","title":{"rendered":"camera permission, side by side ffmpeg"},"content":{"rendered":"\n<p>have been playing around with writing Canon R5C to HDMI capture using swift as a CLI on Mac instead of using ffmpeg, and struggled figuring out why my camera capture code was not running<\/p>\n\n\n\n<p>note to self: enable camera permissions for the CLI<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"653\" src=\"https:\/\/www.visitnyc.store\/blog\/wp-content\/uploads\/2026\/01\/Screenshot-2026-01-18-at-11.55.56-AM-1024x653.png\" alt=\"\" class=\"wp-image-188\" srcset=\"https:\/\/www.visitnyc.store\/blog\/wp-content\/uploads\/2026\/01\/Screenshot-2026-01-18-at-11.55.56-AM-1024x653.png 1024w, https:\/\/www.visitnyc.store\/blog\/wp-content\/uploads\/2026\/01\/Screenshot-2026-01-18-at-11.55.56-AM-300x191.png 300w, https:\/\/www.visitnyc.store\/blog\/wp-content\/uploads\/2026\/01\/Screenshot-2026-01-18-at-11.55.56-AM-768x490.png 768w, https:\/\/www.visitnyc.store\/blog\/wp-content\/uploads\/2026\/01\/Screenshot-2026-01-18-at-11.55.56-AM-1536x980.png 1536w, https:\/\/www.visitnyc.store\/blog\/wp-content\/uploads\/2026\/01\/Screenshot-2026-01-18-at-11.55.56-AM.png 1706w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/bin\/zsh\nset -euo pipefail\n\nDEVICE_NAME=\"Cam Link 4K\"\n\nSTILL_MODE=0\nVR180_MODE=0\n\n# --- args ---\nfor arg in \"$@\"; do\n  case \"$arg\" in\n    -still) STILL_MODE=1 ;;\n    -vr180) VR180_MODE=1 ;;\n    *) ;;\n  esac\ndone\n\n# --- Color correction (Canon Log 3 -&gt; display) ---\n# Put your .cube somewhere stable; choose one that matches your camera gamut setting\nLUT_PATH=\"$HOME\/canon-test-jan-17-2026\/CanonLog3_to_Canon709.cube\"\n\n# tetrahedral looks best; trilinear is faster if you need it\nCC_VF=\"lut3d=file='${LUT_PATH}':interp=tetrahedral\"\n# Tag output for players (metadata only)\nTAG709_VF=\"setparams=color_primaries=bt709:color_trc=bt709:colorspace=bt709:range=tv\"\n\n\nget_avfoundation_video_index() {\n  local name=\"$1\"\n  local out\n\n  out=\"$(ffmpeg -hide_banner -f avfoundation -list_devices true -i \"\" 2&gt;&amp;1 || true)\"\n\n  printf '%s\\n' \"$out\" | awk -v name=\"$name\" '\n    \/AVFoundation video devices:\/ {in_video=1; next}\n    \/AVFoundation audio devices:\/ {in_video=0}\n    in_video &amp;&amp; index($0, name) {\n      if (match($0, \/\\&#91;&#91;0-9]+\\]\/)) {\n        s = substr($0, RSTART, RLENGTH)\n        gsub(\/&#91;^0-9]\/, \"\", s)\n        print s\n        exit\n      }\n    }\n  '\n}\n\nidx=\"$(get_avfoundation_video_index \"$DEVICE_NAME\")\"\n\nif &#91;&#91; -z \"$idx\" ]]; then\n  print -u2 \"Error: could not find avfoundation video device named '$DEVICE_NAME'.\"\n  print -u2 \"Video devices seen:\"\n  ffmpeg -hide_banner -f avfoundation -list_devices true -i \"\" 2&gt;&amp;1 \\\n    | sed -n '\/AVFoundation video devices:\/,\/AVFoundation audio devices:\/p' &gt;&amp;2 || true\n  exit 1\nfi\n\ninput=\"${idx}:none\"\nprint -u2 \"Using '$DEVICE_NAME' at video index ${idx} (ffmpeg -i \\\"$input\\\")\"\n\nmkdir -p hls_raw\n\n# Capture settings (request a device-supported pixel format)\nINPUT_FPS=30\nINPUT_SIZE=\"3840x2160\"\nINPUT_PIXFMT=\"nv12\"   # supported by avfoundation for many devices, avoids yuv420p override warnings\n\n# Your UI-trim crop (y=70, remove 130px total from 2160 -&gt; 2030)\nRAW_CROP_VF='crop=in_w:in_h-130:0:70'\n\n# VR180 prep crop: force 2:1 so each eye becomes square (3840x2160 -&gt; 3840x1920)\n# This makes v360 happier for fisheye mapping.\nVR_SQUARE_CROP_VF='crop=in_w:in_w\/2:0:(in_h-in_w\/2)\/2'\n\n# v360 mapping settings\nVR_FOV=190         # try 180\u2013200; adjust by eyeballing\nVR_YAW=0\nVR_PITCH=0\nVR_ROLL=0\n\n# If your stereo looks \"inside-out\", you may need to swap eyes:\n# SWAP_EYES_VF='stereo3d=sbsr:sbsl,'\nSWAP_EYES_VF=''\n\n# High-res VR180 filterchain (nice for stills)\nVR180_VF=\"${VR_SQUARE_CROP_VF},${SWAP_EYES_VF}v360=input=fisheye:output=hequirect:in_stereo=sbs:out_stereo=sbs:w=3840:h=3840:ih_fov=${VR_FOV}:iv_fov=${VR_FOV}:yaw=${VR_YAW}:pitch=${VR_PITCH}:roll=${VR_ROLL},setsar=1\"\n\n# Streaming VR180 filterchain:\n# Use a smaller per-eye size so SBS total width stays in a VideoToolbox-friendly range.\n# Also force format=nv12 at the end for the encoder.\nVR180_STREAM_VF=\"${VR_SQUARE_CROP_VF},${SWAP_EYES_VF}v360=input=fisheye:output=hequirect:in_stereo=sbs:out_stereo=sbs:w=2048:h=2048:ih_fov=${VR_FOV}:iv_fov=${VR_FOV}:yaw=${VR_YAW}:pitch=${VR_PITCH}:roll=${VR_ROLL},setsar=1,${CC_VF},${TAG709_VF},format=${INPUT_PIXFMT}\"\n\nif (( STILL_MODE )); then\n  print -u2 \"STILL mode: writing single frame to hls_raw\/snapshot.jpg\"\n\n  ffmpeg \\\n    -f avfoundation -thread_queue_size 1024 \\\n    -framerate \"${INPUT_FPS}\" -video_size \"${INPUT_SIZE}\" -pixel_format \"${INPUT_PIXFMT}\" \\\n    -use_wallclock_as_timestamps 1 -i \"$input\" \\\n    -vf \"$RAW_CROP_VF\" \\\n    -frames:v 1 -q:v 2 -y \\\n    hls_raw\/snapshot.jpg\n\n  if (( VR180_MODE )); then\n    print -u2 \"STILL + VR180 mode: writing VR180 half-equirect SBS to hls_raw\/snapshot_vr180.jpg\"\n\n    ffmpeg \\\n      -f avfoundation -thread_queue_size 1024 \\\n      -framerate \"${INPUT_FPS}\" -video_size \"${INPUT_SIZE}\" -pixel_format \"${INPUT_PIXFMT}\" \\\n      -use_wallclock_as_timestamps 1 -i \"$input\" \\\n      -vf \"$VR180_VF\" \\\n      -frames:v 1 -q:v 2 -y \\\n      hls_raw\/snapshot_vr180.jpg\n  fi\n\n  exit 0\nfi\n\nif (( VR180_MODE )); then\n  print -u2 \"VR180 streaming mode: writing HLS to hls_raw\/vr180.m3u8\"\n  print -u2 \"To watch locally:  (cd hls_raw &amp;&amp; python3 -m http.server 8000)  then open http:\/\/localhost:8000\/vr180.m3u8\"\n\n  ffmpeg \\\n    -f avfoundation -thread_queue_size 1024 \\\n    -framerate \"${INPUT_FPS}\" -video_size \"${INPUT_SIZE}\" -pixel_format \"${INPUT_PIXFMT}\" \\\n    -use_wallclock_as_timestamps 1 -i \"$input\" \\\n    -vf \"$VR180_STREAM_VF\" \\\n    -fps_mode cfr -r \"${INPUT_FPS}\" \\\n    -c:v h264_videotoolbox -allow_sw 1 \\\n    -b:v 30M -maxrate 30M -bufsize 60M \\\n    -g 30 \\\n    -force_key_frames \"expr:gte(t,n_forced*1)\" \\\n    -f hls -hls_time 1 -hls_list_size 6 \\\n    -hls_flags delete_segments+append_list+omit_endlist+independent_segments \\\n    -hls_segment_filename \"hls_raw\/vr180_seg%03d.ts\" \\\n    hls_raw\/vr180.m3u8\n\n  exit 0\nfi\n\n# Default: original streaming (but use the same RAW crop as -still so they match)\nprint -u2 \"Streaming mode: writing HLS to hls_raw\/stream.m3u8\"\nprint -u2 \"To watch locally:  (cd hls_raw &amp;&amp; python3 -m http.server 8000)  then open http:\/\/localhost:8000\/stream.m3u8\"\n\nffmpeg \\\n  -f avfoundation -thread_queue_size 1024 \\\n  -framerate \"${INPUT_FPS}\" -video_size \"${INPUT_SIZE}\" -pixel_format \"${INPUT_PIXFMT}\" \\\n  -use_wallclock_as_timestamps 1 -i \"$input\" \\\n  -vf \"$RAW_CROP_VF,$CC_VF,$TAG709_VF,format=${INPUT_PIXFMT}\" \\\n  -fps_mode cfr -r \"${INPUT_FPS}\" \\\n  -c:v h264_videotoolbox -allow_sw 1 \\\n  -b:v 16M -maxrate 16M -bufsize 32M \\\n  -g 30 \\\n  -force_key_frames \"expr:gte(t,n_forced*1)\" \\\n  -f hls -hls_time 1 -hls_list_size 6 \\\n  -hls_flags delete_segments+append_list+omit_endlist+independent_segments \\\n  -hls_segment_filename \"hls_raw\/seg%03d.ts\" \\\n  hls_raw\/stream.m3u8\n<\/code><\/pre>\n\n\n\n<p>heres a script that you run with chmod +x record-stream.sh and then <code>.\/record-stream.sh -vr180<\/code> and it should write to hls_raw\/vr180.m3u8 folder which you should be able to then run a python server <code>python3 -m http.server 8080 --directory hls_raw<\/code><\/p>\n\n\n\n<p>and then from there you can get a side by side projection live from the HDMI cable<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"634\" src=\"https:\/\/www.visitnyc.store\/blog\/wp-content\/uploads\/2026\/01\/Screenshot-2026-01-18-at-11.58.10-AM-1024x634.png\" alt=\"\" class=\"wp-image-189\" srcset=\"https:\/\/www.visitnyc.store\/blog\/wp-content\/uploads\/2026\/01\/Screenshot-2026-01-18-at-11.58.10-AM-1024x634.png 1024w, https:\/\/www.visitnyc.store\/blog\/wp-content\/uploads\/2026\/01\/Screenshot-2026-01-18-at-11.58.10-AM-300x186.png 300w, https:\/\/www.visitnyc.store\/blog\/wp-content\/uploads\/2026\/01\/Screenshot-2026-01-18-at-11.58.10-AM-768x475.png 768w, https:\/\/www.visitnyc.store\/blog\/wp-content\/uploads\/2026\/01\/Screenshot-2026-01-18-at-11.58.10-AM.png 1428w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>take a look at the script, theres a way to get jpeg and a way to see the actual HDMI capture from a canon r5c raw<\/p>\n\n\n\n<p>The camera itself is switched to the display mode where if you press the Display 3(INFO) button, it toggles through the view, its the one where theres no UI except the STBY \/ REC label at the top, and then the <\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/www.visitnyc.store\/blog\/wp-content\/uploads\/2026\/01\/IMG_0300.heic\" alt=\"\" class=\"wp-image-190\"\/><\/figure>\n\n\n\n<p>all with ffmpeg!<\/p>\n\n\n\n<p>the next step in this process is to convert it to a swift app, but has to be investigated if the ffmpeg stream can be loaded into Moon Player or something that can take the ffmpeg stream of side by side and then<\/p>\n\n\n\n<p>oh right another note to self: when ffmpeg is running to quit it enter &#8220;q&#8221; otherwise you might ghost it or it might not want to stop entering &#8220;q&#8221; is the quickest way to get the ffmpeg to knock it off and shutdown<\/p>\n","protected":false},"excerpt":{"rendered":"<p>have been playing around with writing Canon R5C to HDMI capture using swift as a CLI on Mac instead of using ffmpeg, and struggled figuring out why my camera capture code was not running note to self: enable camera permissions for the CLI heres a script that you run with chmod +x record-stream.sh and then [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-187","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/www.visitnyc.store\/blog\/wp-json\/wp\/v2\/posts\/187","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.visitnyc.store\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.visitnyc.store\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.visitnyc.store\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.visitnyc.store\/blog\/wp-json\/wp\/v2\/comments?post=187"}],"version-history":[{"count":2,"href":"https:\/\/www.visitnyc.store\/blog\/wp-json\/wp\/v2\/posts\/187\/revisions"}],"predecessor-version":[{"id":192,"href":"https:\/\/www.visitnyc.store\/blog\/wp-json\/wp\/v2\/posts\/187\/revisions\/192"}],"wp:attachment":[{"href":"https:\/\/www.visitnyc.store\/blog\/wp-json\/wp\/v2\/media?parent=187"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.visitnyc.store\/blog\/wp-json\/wp\/v2\/categories?post=187"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.visitnyc.store\/blog\/wp-json\/wp\/v2\/tags?post=187"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}