Flutterã§ã®360°ãããªã®ãªãã¬ãŒãã³ã°: çé¢æåœ±ãã¯ã©ãŒã¿ããªã³ããŒãã¬ãŒã ãGPUãšã¯ã¹ããŒã
çã®æ£è·åç峿³360°ããããŒãžããFlutterã¢ããªå ã§å®å šã«ã¹ã ãŒãºã§ãã¬ãŒã ä»ãã®ãã©ãããªãããªã«å€æããæ¹æ³ã
誰ãæããŠãããªãåé¡
éåžžã®ãããªã«ã¯ãã«ã¡ã©ãåãããã¬ãŒã ãšãããããªãã«èŠããæçœãªãã®ã1ã€ãããŸãã360°ãããªã«ã¯ãã®ãããªãã®ã¯ãããŸãããã«ã¡ã©ã¯ãã¹ãŠãèšé²ããŸããâX軞ãçµåºŠã§Y軞ã緯床ã§ããå¹³ããªæ£è·åç峿³ïŒ2:1ã®ç»åïŒãšããŠä¿åããããã¯ã»ã«ã®å®å šãªçäœã§ãããã®ããããŒãžãInstagramãYouTubeããŸãã¯é»è©±ã®ç»é¢ã«è¡šç€ºããåã«ãã«ã¡ã©ãæå³çã«çããããšããªãã£ã質åã«èª°ããçããå¿ èŠããããŸãïŒ
èŠèŽè ã¯ã©ããããã€èŠãã¹ããïŒ
ããªãã¬ãŒãã³ã°ããšã¯ãä»®æ³ã«ã¡ã©ããã®çäœã®äžãæéãšãšãã«é£è¡ããâãã³ããã«ãããºãŒã ãè¡ãâéåžžã®ãã©ãããª16:9ïŒãŸãã¯9:16ããŸãã¯2.35:1ïŒãããªãšããŠã¬ã³ããªã³ã°ããè¡çºã§ããããã¯ãGoPro PlayerãInsta360 Studioãããã³Adobe Premiereã®GoPro VR Reframeãã©ã°ã€ã³ãæ§ç¯ãããŠããæ©èœã§ãã
ç§ãã¡ã¯ãããå®å šã«Flutterã§æ§ç¯ããŸããããã®èšäºã¯ãæ¬åœã«é£ããã£ãéšåãããªãã¡æåœ±æ°åŠããžã³ãã«ããã¯ããªãŒè£éã±180°ã®ç¶ãç®ããããŠå®éã®Androidãã³ãŒããŒã§åäœããå¿ èŠãããGPUãšã¯ã¹ããŒããã€ãã©ã€ã³ã«ã€ããŠèª¬æããŸãã
ãšã³ãããŒãšã³ãã®ãããŒïŒ
360ãããªã®ãã£ããã£/ã€ã³ããŒã
â 軜éãªãã¬ãã¥ãŒãããã·ã®ãã©ã³ã¹ã³ãŒã
â ãŠãŒã¶ãŒãã¿ãã/æç»/被åäœãèªåæ€åº
â ãªããžã§ã¯ããã©ããã³ã°ããã¹ãçæ
â ããŒãã¬ãŒã ã®çæ+å¹³æ»å
â ãã¬ãŒã ããšã®ã«ã¡ã©åããSLERPè£éïŒã©ã€ããã¬ãã¥ãŒïŒ
â ãã€ãã£ãGPUã§ãã©ãããªãããªã«ãšã¯ã¹ããŒã
â FFmpegåŸåŠçïŒmoov atom + A/Våæä¿®æ£ïŒ
ã¢ãŒããã¯ãã£ã®æŠèŠ
ç§ãã¡ã¯ã·ã¹ãã ã3ã€ã®æç¢ºãªã¬ã€ã€ãŒã«ä¿ã¡ãæ°åŠãFlutterããç¬ç«ããŠãã¹ãã§ããããã«ããŸããïŒ
| ã¬ã€ã€ãŒ | å Žæ | åœ¹å² |
|---|---|---|
| ã¢ãã« | lib/data/models/reframe/ | SphericalCoordinates/Quaternion, CameraOrientation/MotionConstraints, ReframeKeyframe, ReframeProject |
| ãµãŒãã¹ | lib/services/reframe/ | SphericalProjection (ã¬ã€ãã£ã¹ãã£ã³ã°), CoordinateConverter, KeyframeInterpolator/Generator/Smoother, ReframeExportService |
| ãã¬ãŒã³ããŒã·ã§ã³ | lib/presentation/.../reframe/ | Riverpod ReframeNotifier, ãã€ãã£ãOpenGL Spherical360Player, bboxæç»ãªãŒããŒã¬ã€, ããŒãã¬ãŒã ã¿ã€ã ã©ã€ã³ |
é»éåŸïŒãã¹ãŠã®äžè§æ³ã¯ãFlutterã€ã³ããŒããªãã®çŽç²ãªDartã§ããã€ãŸãããã¹ãŠã®æåœ±ããã³è£é颿°ã¯ããŠã£ãžã§ããããªãŒãªãã§ãŠããããã¹ãå¯èœã§ãããåãæ°åŠãã©ã€ããã¬ãã¥ãŒãšãšã¯ã¹ããŒãã®äž¡æ¹ãé§åããŸãã
1. ãã®æ žå¿ïŒæ£è·åç峿³ â· çé¢
ãã¹ãŠã¯äžã€ã®ãããã³ã°ããå§ãŸããŸããæ£è·åç峿³ãã¬ãŒã ã¯ãåãªãé·æ¹åœ¢ã§ãããããã§ã¯ïŒ
- XïŒ0 â å¹ ïŒã¯çµåºŠïŒãšãŒïŒãâ180°ãã+180°ãŸã§æåŒããŸã
- YïŒ0 â é«ãïŒã¯ç·¯åºŠïŒãããïŒã+90°ïŒäž/倩é ïŒããâ90°ïŒäž/倩åºïŒãŸã§æåŒããŸã
SphericalCoordinatesã¯æå³çã«å°ããããšãŒãšãããã®ã¿ã§ããããŒã«ãFOVããºãŒã ã¯æ¹åã§ã¯ãªãã«ã¡ã©ã«å±ããŸãããããæ©èœå šäœã®åºç€ãšãªã倿ã§ãã
factory SphericalCoordinates.fromEquirectangular({
required double x, required double y,
required double videoWidth, required double videoHeight,
}) {
final normalizedX = x / videoWidth;
final normalizedY = y / videoHeight;
// Xã¯ãšãŒã«ãããã³ã°ãããŸã: 0->-180°ã0.5->0°ã1.0->180°
final yaw = (normalizedX - 0.5) * 360.0;
// Yã¯ãããã«ãããã³ã°ãããŸã: 0->90° (äž)ã0.5->0° (å°å¹³ç·)ã1.0->-90° (äž)
final pitch = (0.5 - normalizedY) * 180.0;
return SphericalCoordinates(
yaw: yaw.clamp(-180.0, 180.0),
pitch: pitch.clamp(-90.0, 90.0),
);
}
é倿ã¯ãã©ãŒã€ã¡ãŒãžïŒnormalizedX = yaw/360 + 0.5ïŒã§ããã远跡ãããããŠã³ãã£ã³ã°ããã¯ã¹ããã¬ã€ã€ãŒã«æãæ»ãããã«åžžã«äœ¿çšããŸãã
ãèŠãŠãããã®ãã¿ããã â éèŠã¬ã€ãã£ã¹ãã£ã³ã°
ãŠãŒã¶ãŒã¯æ£è·åç峿³ã®é·æ¹åœ¢ãšçŽæ¥å¯Ÿè©±ããŸããã圌ãã¯ã¬ã³ããªã³ã°ãããéèŠãã¥ãŒãšå¯Ÿè©±ããŸãããããã£ãŠããã¬ã€ã€ãŒäžã®ç¹ãã¿ãããããšãä»®æ³ãã³ããŒã«ã«ã¡ã©ãéããŠã¬ã€ããã£ã¹ãããçäœäžã®ã©ã®æ¹åããããããããèŠã€ããå¿ èŠããããŸãããããSphericalProjection.screenToSphericalã§ãã
final ndcX = (2.0 * screenPoint.dx / playerSize.width) - 1.0;
final ndcY = 1.0 - (2.0 * screenPoint.dy / playerSize.height);
final aspectRatio = playerSize.width / playerSize.height;
final tanHalfFov = math.tan(cameraFov * math.pi / 360.0); // half-angle
double rayX = ndcX * aspectRatio * tanHalfFov;
double rayY = ndcY * tanHalfFov;
double rayZ = 1.0;
// ...æ£èŠåããã¬ã€ããããïŒX軞åšãïŒã§å転ãããæ¬¡ã«ãšãŒïŒY軞åšãïŒã§å転ããã...
final sphericalYaw = math.atan2(finalRayX, finalRayZ) * 180.0 / math.pi;
final sphericalPitch = math.asin(finalRayY.clamp(-1.0, 1.0)) * 180.0 / math.pi;
.clamp(-1.0, 1.0)ã¯asinã®åã«ä»ãããšéå°ã«èŠãããããããŸããããæµ®åå°æ°ç¹æ°ã®ããªããã«ãã1.0000001ãæž¡ãããasinã¯NaNãè¿ããŸã â ãããŠåäžã®NaNã¯ãã¹ãŠã®äžæµãã¬ãŒã ãæ±æããŸããéäžè§é¢æ°ã®åšå²ã§é²åŸ¡çã«ã¯ã©ã³ãããããšã¯ããã®ã³ãŒãããŒã¹ã«ãããç¹°ãè¿ãçŸããããŒãã§ãã
2. ã¹ã ãŒãºãªã«ã¡ã©ã¢ãŒã·ã§ã³ïŒãªã€ã©ãŒè§ã§ã¯ãªãã¯ã©ãŒã¿ããªã³
çŽ æŽãªã¢ãããŒãã¯ãåããŒãã¬ãŒã ã§ãšãŒ/ããããä¿åããæ°å€ãç·åœ¢è£éããããšã§ããããã¯ã²ã©ãèŠããŸãããªã€ã©ãŒè§ãè£éãããšããžã³ãã«ããã¯ãéäžæ§ãªè§é床ãããã³æ¥µä»è¿ã§ã®äžæ Œå¥œãªã¹ããããçºçããŸãã
ç§ãã¡ã¯ãSLERPïŒçé¢ç·åœ¢è£éïŒã䜿çšããŠåããã¯ã©ãŒã¿ããªã³ãšããŠè£éããŸããåæ¹åã¯ãZYX EulerèŠçŽãä»ããŠã¯ã©ãŒã¿ããªã³ã«å€æãããŸãïŒ
Quaternion toQuaternion() {
final yawRad = yaw * math.pi / 180.0;
final pitchRad = pitch * math.pi / 180.0;
final cy = math.cos(yawRad * 0.5); final sy = math.sin(yawRad * 0.5);
final cp = math.cos(pitchRad * 0.5); final sp = math.sin(pitchRad * 0.5);
return Quaternion(w: cy * cp, x: cy * sp, y: sy * cp, z: -sy * sp);
}
ãããŠãSLERPèªäœã«ã¯ããã¹ãŠã®å®è£ ãå¿ èŠãšãã2ã€ã®å®éçšã¬ãã«ã®å®å šåŒããããŸãïŒ
static Quaternion slerp(Quaternion a, Quaternion b, double t) {
a = a.normalize(); b = b.normalize();
var dotProduct = a.dot(b);
if (dotProduct < 0.0) { b = -b; dotProduct = -dotProduct; } // æçã®åçã䜿çš
if (dotProduct > 0.9995) { // ã»ãŒå¹³è¡ -> LERPã«ãã©ãŒã«ããã¯
return Quaternion(w: a.w + t*(b.w-a.w), x: a.x + t*(b.x-a.x),
y: a.y + t*(b.y-a.y), z: a.z + t*(b.z-a.z)).normalize();
}
final theta0 = math.acos(dotProduct);
final theta = theta0 * t;
final sinTheta = math.sin(theta);
final sinTheta0 = math.sin(theta0);
final s0 = math.cos(theta) - dotProduct * sinTheta / sinTheta0;
final s1 = sinTheta / sinTheta0;
return Quaternion(w: s0*a.w + s1*b.w, /* ...*/).normalize();
}
2ã€ã®é亀æžçãªè©³çްïŒ
- æçã®åç â qãšâqã¯åãåãã衚ããŸãããããç©ãè² ã®å Žåãããããã®å ¥åãåŠå®ããªããšãã«ã¡ã©ãçäœã®åšããé åãããŸãã
- ã»ãŒå¹³è¡ãªå Žåã®LERPãã©ãŒã«ãã㯠â 2ã€ã®ããŒãã¬ãŒã ãã»ãŒåäžã®å ŽåãsinTheta0 â 0ãšãªããã»ãŒãŒãã§é€ç®ããããšã«ãªããŸããdot > 0.9995ã§åçŽãªLERPã«ãã©ãŒã«ããã¯ããããšã§ãç Žç¶»ãé²ããŸãã
埮åŠã ãéèŠãªèšèšäžã®å€æïŒç§ãã¡ã¯æ¹åã¯SLERPã§è£éããFOV/ãºãŒã ã¯LERPã§è£éããããŒã«ã¯è§åºŠLERPã§è£éããŸããçé¢è£éã¯æ¹åã«ã¯é©åãªããŒã«ã§ããããºãŒã ã¯åãªãã¹ã«ã©ãŒã§ãããç·åœ¢ã«ç§»åããå¿ èŠããããŸãïŒ
final qInterp = Quaternion.slerp(from.direction.toQuaternion(), to.direction.toQuaternion(), t);
final interpDirection = SphericalCoordinates.fromQuaternion(qInterp).normalize();
final interpFov = _lerp(from.fov, to.fov, t);
final interpRoll = _lerpAngle(from.roll, to.roll, t); // ±180°ãè¶ããŠã©ãã
final interpZoom = _lerp(from.zoom, to.zoom, t);
ð¡ ã¯ã©ãŒã¿ããªã³ãããšãŒ/ãããã«æ»ãéã«ã¯ããæ°å€å®å®æ§ã®ããïŒasinã®ãã¡ã€ã³åé¡ãåé¿ããããïŒãasinã§ã¯ãªãatan2ã䜿çšããŸããSLERPã®åŸãã¯ã©ãŒã¿ããªã³ã¯ãããããã«éæ£èŠåãããå¯èœæ§ããããasinãNaNãæããã®ã«å¯Ÿããatan2㯠gracefullyã«å£åããŸãã
ã€ãŒãžã³ã°ïŒæåã§äœæãå ¥ã£ãŠãããã©ã³ãžã·ã§ã³ã«
åããŒãã¬ãŒã ã¯ããããžã®ãã©ã³ãžã·ã§ã³ã®ã€ãŒãžã³ã°ã«ãŒããææããŸããã«ãŒãã¯ææžãã§èšè¿°ãããŠããããããã¹ãŠã®ãã©ãããã©ãŒã ã§åäžã§ãïŒ
case KeyframeEasing.linear: return t;
case KeyframeEasing.easeInOut: return t * t * (3 - 2 * t); // smoothstep Såã«ãŒã
case KeyframeEasing.easeIn: return t * t; // äºæ¬¡é¢æ°
case KeyframeEasing.easeOut: return 1 - (1 - t) * (1 - t);
3. ±180°ã®ç¶ãç®ïŒãã¹ãŠã®360°æ©èœãæ©ãŸãããã°
ããã«èœãšã穎ããããŸãããšãŒã¯åäžã«ååšããŸãïŒ+179°ãšâ179°ã¯358°ã§ã¯ãªã2°é¢ããŠããŸãã被åäœãçäœã®åŸããæšªåã£ãç¬éãçŽ æŽãªèšç®ã§ã¯ã«ã¡ã©ãå°çã®ååšãã¹ãããããŠããŸããŸãããã®åäžã®åé¡ã¯ã·ã¹ãã ã®4ã€ã®ç°ãªãã¬ã€ã€ãŒã§çŸããããããã«ç¬èªã®ä¿®æ£ãå¿ èŠã§ãã
ã¬ã€ã€ãŒ1 â ããªããã£ããã³ãŒãããŒã¹å šäœãäŸåãã2ã€ã®å°ããªãã«ããŒïŒ
static double normalizeYaw(double yaw) {
while (yaw > 180) { yaw -= 360; }
while (yaw < -180) { yaw += 360; }
return yaw;
}
// ãç¶ãç®å®å
šãªãã©ããã³ã°ã«äžå¯æ¬ ...180°ã¹ããããåé¿ã
static double shortestYawDelta(double from, double to) {
double delta = to - from;
if (delta > 180) delta -= 360;
if (delta < -180) delta += 360;
return delta;
}
ã¬ã€ã€ãŒ2 â ããŒãã¬ãŒã çæã¯ãé£ç¶çãªïŒç¡å¶éã®ïŒãšãŒãèç©ãããããSLERPã¯åžžã«æççµè·¯ãè£éããŸãããã¹ãŠã®ãµã³ãã«ã±180°ã«ã¯ã©ã³ããã代ããã«ãæçã®ãã«ã¿ãåèšããæå³çã«å®è¡å€ã180°ãè¶ ããŠããªãããããŸãïŒ
final normalizedContinuousYaw = SphericalCoordinates.normalizeYaw(continuousYaw);
final yawDelta = SphericalCoordinates.shortestYawDelta(normalizedContinuousYaw, rawCoords.yaw);
continuousYaw = continuousYaw + yawDelta; // æå³çã«Â±180°ãè¶
ããå Žåãã
coords = SphericalCoordinates(yaw: continuousYaw, pitch: rawCoords.pitch);
ã¬ã€ã€ãŒ3 â ããŠã³ãã£ã³ã°ããã¯ã¹ã®åæ§ç¯ããŠãŒã¶ãŒãç¶ãç®ããŸããããã¯ã¹ãæç»ãããšã4ã€ã®ã³ãŒããŒã¯+170°ã+175°ãâ175°ãâ170°ã®ãããªãšãŒãå ±åããŸããSphericalProjectionã¯ãããæ€åºãïŒä»»æã®ã³ãŒã㌠> 90°ãã€ä»»æã®ã³ãŒã㌠< â90°ïŒãè² ã®å€ã+360°ã·ããããŠãããã®ã¹ãã³ã枬å®ããããšã§ãé£ç¶ãšãŒã®ãç¶ãç®å®å šãªãbboxãåæ§ç¯ããŸãã
ã¬ã€ã€ãŒ4 â ã©ã€ããã¬ã€ã€ãŒã¯shortestYawDeltaã䜿çšããŠã¿ãŒã²ããã«åãã£ãŠã¹ã ãŒãºã«ç§»åãããããç¶ãç®ã暪åããã©ãã°ã§ãããããããããšã¯ãããŸããã
360°ããŒã«ãæ§ç¯ããéã«äžã€èŠããŠããã¹ãããšïŒç¶ãç®ã¯ãšããžã±ãŒã¹ã§ã¯ãªããæšªæçãªé¢å¿äºã§ãããšãŒãåºçŸããããããå Žæã§èæ ®ããŠãã ããã
4. Insta360/GoProã®ãããªæèŠã«ãã
ã¹ã ãŒãºãªè£éã¯ãæè¡çã«æ£ããã«ã¡ã©ããããããŸããããããè¯ãã«ã¡ã©ã§ã¯ãããŸããããæèŠå·¥åŠãã®3ã€ã®èŠçŽ ããã®ã®ã£ãããåããŸãã
ã¢ãŒã·ã§ã³å¶çŽ â é床ãšå é床ãã¯ã©ã³ãããŸããçã®è¿œè·¡ããŒã¿ã¯äžå®å®ã§ããç§ãã¡ã¯ããŒãã¬ãŒã ãMotionConstraintsã«éããŸããããã¯ä»®æ³ã«ã¡ã©ãã©ãã ãéããã©ãã ãçªç¶åããããå¶éããŸãïŒäŸïŒmaxYawSpeed = 120°/ç§ãmaxPitchSpeed = 90°/ç§ãmaxAcceleration = 180°/ç§Â²ïŒãcinematic()ãresponsive()ãproduction()ã®ããªã»ãããçšæãããŠããŸããããã¯ãããšã远跡ããç£èŠã«ã¡ã©ããšãå®å®ããæãæã€ãªãã¬ãŒã¿ãŒãã®éãã§ãã
é©å¿åFOV â 被åäœã«åãããããã®ãºãŒã ãæ£è·åç峿³ç©ºéã§è¢«åäœã®è§ãµã€ãºã枬å®ãããã¬ãŒã ã®ç®æšå²åãæºããããã«FOVãéžæããŸãïŒ
final angularWidth = (bboxWidth / videoWidth) * 360.0;
final angularHeight = (bboxHeight / videoHeight) * 180.0;
final objectAngularSize = math.max(angularWidth, angularHeight);
final idealFov = objectAngularSize / targetFillRatio; // targetFillRatio â 0.4
return idealFov.clamp(minFov, maxFov); // 45°â120°
ã¢ãŒã·ã§ã³ãªãŒã + é¡âäœãã¬ã³ããç§ãã¡ã¯ã被åäœãé²ãæ¹åã®bboxå¹ ã®çŽ15%ãå è¡ãããŠãã¬ãŒãã³ã°ãç§»åãããŸããããã«ããã被åäœãæ©ããŠããæ¹åã®ç«¯ã«åºå®ãããã®ãé²ããŸãããããŠé¡æ€åºãéåããå Žåã§ããäœã®äžå¿ã«ã¹ãããããã®ã§ã¯ãªããæåŸã«èªèãããé¡ã®äžå¿ããäœã®äžå¿ãžçŽ10ãã¬ãŒã ãããŠã©ãŒãããŸãã
5. 2ã€ã®ãããªããªãã¯ïŒãã¬ãã¥ãŒãããã·å¯Ÿãã«è§£å床ãªãªãžãã«
ããã¯ãã·ã¹ãã å šäœãå®éã®é»è©±ã§å©çšå¯èœã«ããããã®ã¢ãŒããã¯ãã£äžã®æ±ºå®ã§ãã
360° GoPro/Insta360ãã¡ã€ã«ã¯ããã°ãã°4096Ã2048 HEVCã§ããäœäºç®ã®Androidãã³ãŒããŒã¯ããªããžã§ã¯ããã©ããã³ã°ãåæã«å®è¡ããã©ãããããããã¹ã ãŒãºã«åçããããšããã ãã§è©°ãŸã£ãŠããŸããŸãããã®ãããç§ãã¡ã¯ãã©ã³ã¹ã³ãŒãããããããã·ïŒçŽ1440Ã720ãlibx264 -preset veryfast -crf 28ïŒã§ãã©ããã³ã°ãšã©ã€ããã¬ãã¥ãŒãå®è¡ããæçµãšã¯ã¹ããŒãã®ã¿ã«ãã«è§£å床ã®ãªãªãžãã«ãäºçŽããŸãã
// ã360°ãããªã®MediaMetadataRetrieverãããä¿¡é Œæ§ãé«ã
// ïŒHEVCãé«è§£å床ãªã©ïŒã®çããã³ãŒããã¯ã䜿çšããããšãå€ãããã
final maxSafeWidth = 2048, maxSafeHeight = 1080;
// 2:1 4K (4096Ã2048)ã¯å®å
šã§ãªã -> transcodeForPlayback()ãããªã¬ãŒ
泚æãã¹ãç¹ïŒããŒãã¬ãŒã ã®ããŠã³ãã£ã³ã°ããã¯ã¹ã¯ãããã·åº§æšã§èšç®ããããšã¯ã¹ããŒãåã«å ã®åº§æšã«ã¹ã±ãŒã«ã¢ããããå¿ èŠããããŸãããã®ã¹ã±ãŒãªã³ã°ãééãããšããšã¯ã¹ããŒãããããã¬ãŒã ã¯ãŠãŒã¶ãŒããã¬ãã¥ãŒããé åãšãããã«ç°ãªããã®ã«ãªããŸããïŒã¡ã¿ããŒã¿ãšæåã®ãã¬ãŒã æœåºã«ã¯ãMediaMetadataRetrieverã§ã¯ãªãffprobeã䜿çšããŠããŸã â Androidã®å èµã¬ããªãŒããŒã¯ã360°ã«ã¡ã©ãçæããçããã³ãŒããã¯ã§ã¯äžå®å®ã§ããïŒ
6. GPUãšã¯ã¹ããŒãïŒMethodChannelãä»ããOpenGL + MediaCodec
é»è©±ã®CPUäžã§FFmpegã®v360ãã£ã«ã¿ãŒãä»ããŠ4Kçã®ãã¹ãŠã®ãã¬ãŒã ãåæåœ±ããã®ã¯ãã¯ããã«é ãããŸããïŒç§ãã¡ã¯v360æååãä¿æããŠããŸãâããã¯æåœ±ã®æå³è«ãã€ãŸãv360=e:flat:...ããæ£è·åç峿³å ¥åããã©ããåºåããæå³ããããšããããã«ææžåããŠããŸãããã©ã€ããã¹ã§ã¯ãããŸãããïŒ
å®éã®ãšã¯ã¹ããŒãã¯ãMethodChannelãä»ããŠããŒãã¬ãŒã ããã€ãã£ãã®OpenGL ES + MediaCodecã¬ã³ãã©ãŒã«æž¡ããŸãïŒ
final result = await _methodChannel.invokeMethod('exportVideoGPU', {
'videoPath': resolvedVideoPath,
'outputPath': outputPath,
'keyframes': keyframesList
'initialOrientation': initialOrientationMap,
'width': settings.exportWidth,
'height': settings.exportHeight,
'fps': settings.outputFps.toInt(),
'includeAudio': settings.includeAudio,
if (isTrimmed) 'trimStartMs': trimStartMs,
if (isTrimmed) 'trimEndMs': trimEndMs
});
ç§ãã¡ã¯ãããæ©èœãã§ãã¯ïŒAndroid API 21+ãããŒããŠã§ã¢H.264ãšã³ã³ãŒããŒãOpenGL ES 3.0ïŒã®èåŸã«çœ®ããEventChannelãä»ããŠé²æç¶æ³ãã¹ããªãŒã ã§è¿ããŸããããã€ãèŠåŽããŠåŠãã èœãšã穎ããããŸãïŒ
- â90°ã®ãšãŒãªãã»ãããç§ãã¡ã®Dartã®èŠçŽã§ã¯ããšãŒ = 0ãæ£è·åç峿³ã®ç»åã®äžå€®ïŒu = 0.5ïŒã«é 眮ããŸããOpenGLã¬ã³ãã©ãŒã¯ããšãŒ = 0ãu = 0.75ã«é 眮ããŸãããã®ããããã¹ãŠã®ããŒãã¬ãŒã ã®ãšãŒã¯ããã£ã³ãã«ãè¶ããéã«â90°ïŒãããŠÂ±180°ã«åã©ããïŒã§åãããã³ã°ãããŸã â ããã¯ãšã¯ã¹ããŒããã£ã³ãã«ãšã©ã€ããã¬ã€ã€ãŒãã£ã³ãã«ã®äž¡æ¹ã«å¯Ÿç§°çã«é©çšããããã¬ãã¥ãŒãšãšã¯ã¹ããŒããäžèŽããããã«ããŸãã
- éåããããŒãã¬ãŒã æ°ãããŒãã¬ãŒã ããŒãã®å Žå â åæå§¿å¢ãã2ã€ã®åäžã®ããŒãã¬ãŒã ãåæïŒéçãšã¯ã¹ããŒãïŒãããŒãã¬ãŒã ã1ã€ã®å Žå â ãããè€è£œããŸããã¬ã³ãã©ãŒã¯åžžã«å°ãªããšãéå§ãšçµäºãå¿ èŠãšããŸãã
- ããªãã³ã°ã¯ã¿ã€ã ã¹ã¿ã³ããåããŒã¹åããŸããããªãã³ã°ããããšã¯ã¹ããŒãã¯ããã¹ãŠã®ããŒãã¬ãŒã ãt â trimStartMsã ãã·ããããŸãã
誰ãäºæ³ããªãFFmpegåŸåŠç
GPUãšã¯ã¹ããŒãããŽãŒã«ã ãšæããããããããŸãããããããããã§ã¯ãããŸãã â Androidã®MediaMuxerã¯moov atomããã¡ã€ã«ã®æ«å°Ÿã«æžã蟌ã¿ããœãŒã¹ã®ãªãŒãã£ãªPTSãªãã»ãããéã¶ããšãã§ããããã§ãããã®çµæïŒExoPlayerïŒããã³Flutterã®video_playerïŒã¯ãã¯ããã¯ãåãç¶ããŠããéãç»åãããªãŒãºãããŸãããã®ãããGPUãã¹ã®åŸãFFmpegã¯é«éã§ãã¹ã¬ã¹ãªã¯ãªãŒã³ã¢ãããè¡ããŸãïŒ
// ãããªãã³ããŒïŒåãšã³ã³ãŒããªã -> å質å£åãŒãïŒããªãŒãã£ãªãåãšã³ã³ãŒãããŠPTSãä¿®æ£ã
// moov atomãå
é ã«ç§»åãããŠå³æåçãå¯èœã«ããã
-c:v copy -af "aresample=async=1:first_pts=0" -movflags +faststart
ãããŠããããå®ãæ®é ·ãªå°ããªèœãšãç©ŽïŒæåã«ãªãŒãã£ãªãã©ãã¯ãæ€åºããŸãããªããªãããªãŒãã£ãªãã©ãã¯ããªããããªã«ãªãŒãã£ãªãã©ã°ãæž¡ããšãäžéšã®ããã€ã¹ã§FFmpegãæ°žä¹ ã«ãã³ã°ããããã§ããåŸåŠçå šäœã¯ãã¿ã€ã ã¢ãŠãæã«ãã£ã³ã»ã«ãã60ç§ã®ã¿ã€ã ã¢ãŠãã§ã©ãããããŠããŸãã
7. ã©ã€ããã¬ãã¥ãŒããã€ãã£ããã¬ã€ã€ãŒã§ã
ãšãã£ã¿å ã®ãã¬ãã¥ãŒã¯Flutter CustomPaintã§ã¯ãããŸãã â AndroidViewïŒ'your_app/spherical360player'ã®ãããªviewTypeãæã€ïŒãä»ããŠåã蟌ãŸãããã€ãã£ãã®OpenGLãµãŒãã§ã¹ã§ããããã¥ãŒããšã®ç¬èªã®ã¡ãœãã/ã€ãã³ããã£ã³ãã«ãæã£ãŠããŸããDart Spherical360PlayerControllerã¯ãšãŒ/ããã/ããŒã«/FOVããã©ãŒãªã³ã°ããéä¿¡åã«åãâ90°ãªãã»ãããé©çšããŸãã
å¿«é©ã«ããããã®ãã£ãŒãªã³ã°ã®è©³çްïŒ
- ãã¬ãŒã ã¬ãŒãéäŸåã®ã¹ã ãŒãžã³ã° â factor = 1 - exp(-speed * dt)ãªã®ã§ã60fpsã§ã90fpsã§ãã€ãŒãžã³ã°ã¯åãã«èŠããç¶ãç®å®å šãªæççµè·¯ãšãŒãçµã¿èŸŒãŸããŠããŸãã
- ããªãã¯åŸã®æ £æ§/æ°æ§ãæ©æŠããŒã¹ã®æžè¡° â åçäžã¯ç¡å¹ããªããªãåçäžã¯ã«ã¡ã©ã¯ãŠãŒã¶ãŒã®æåŸã®ããªãã¯ã§ã¯ãªãããŒãã¬ãŒã ã«åŸãå¿ èŠãããããã§ãã
- äžã€ã®GestureDetectorããã®onScaleUpdateã¯ãã³ãšãã³ããºãŒã ãåæã«è¡ããŸããããã«ã¿ããã§åç/äžæåæ¢ãåãæ¿ããŸããFOVã¯30°ã120°ã«ããããã¯Â±90°ã«ãã³ã³ãããŒã©å¢çã§ã¯ã©ã³ããããŸãã
äžã€å°ããªããããéèŠãªUXã®ä¿®æ£ïŒããã©ããã³ã°äžããããããŒãã¬ãŒã çæäžãã«åãæ¿ããåã«ãFlutterãå®éã«ã100%ããæç»ããããã«120ããªç§ã®é å»¶ãæ¿å ¥ããŸããããããªããšãæçµçãªé²æãã¬ãŒã ãšã¹ããŒã¿ã¹å€æŽãåããã€ã¯ãã¿ã¹ã¯ãããã«å ¥ãããŠãŒã¶ãŒã¯å®äºãèŠãããšã«ãªããŸããã
ã¹ããŒããã·ã³ãç°¡åã«èšããš
ããããã¹ãŠã¯ãRiverpod ReframeNotifierãã¹ããŒã¿ã¹enumãé§åããããšã§èª¿æŽãããŸãïŒ
idle â ãããªèªã¿èŸŒã¿äž â ãã©ã³ã¹ã³ãŒãäž
â (æ€åºäž | ãªããžã§ã¯ãéžæäž | ãã¬ã€ã€ãŒã«bboxæç»äž | ãã¬ã€ã€ãŒã§èªåæ€åºäž)
â ãã©ããã³ã°äž â ããŒãã¬ãŒã çæäž â ç·šéäž â ãšã¯ã¹ããŒãäž â å®äº | ãšã©ãŒ
è€æ°ã®ãšã³ããªãŒãã€ã³ããåããã©ãã«ãŒã«äŸçµŠãããŸãïŒã¿ããããŠæ€åºïŒYOLOãªãŒããŒã¬ã€ïŒãæåbboxããŸãã¯Insta360ã¹ã¿ã€ã«ã®ãã¬ã€ã€ãŒã«çŽæ¥æç»ãããžã§ã¹ãã£ãŒããããã¯ãã¹ãŠKeyframeGenerator.production().generate(...) â KeyframeSmoother.smooth(...)ã«åæããããããè£éåšããã¹ãŠã®ãã¬ãã¥ãŒããã³ãšã¯ã¹ããŒããã¬ãŒã ãé§åããŸãã
åŠãã ããš
- æ°åŠã¯çŽç²ã«ä¿ã€ãæåœ±/è£éã¬ã€ã€ãŒã«ã¯Flutterã€ã³ããŒããå«ããªããããããã£ããããããã¹ãå¯èœã§ããã
- æ¹åã«ã¯ã¯ã©ãŒã¿ããªã³SLERPïŒã¹ã«ã©ãŒã«ã¯LERPããªã€ã©ãŒè§ãè£éããŠã¯ãããŸãããããºãŒã ãSLERPããŠã¯ãããŸããã
- ±180°ã®ç¶ãç®ã¯ãšããžã±ãŒã¹ã§ã¯ãªããæšªæçãªé¢å¿äºã§ããããªããã£ããçæãæåœ±ããã¬ã€ã€ãŒã§ååºçŸããŸãã
- äžåºŠã察称çã«èŠçŽã調æŽãããDartãšOpenGLéã®â90°ãšãŒãªãã»ããã¯ããã¬ãã¥ãŒãšãšã¯ã¹ããŒããäžèŽããããã«ãäž¡æ¹ã®ãã£ã³ãã«å¢çã§é©çšãããŸãã
- ã€ã³ã¿ã©ã¯ã·ã§ã³ã«ã¯ãããã·ãåºåã«ã¯ãªãªãžãã«ãäž¡è ã®éã§åº§æšãã¹ã±ãŒã«ã¢ããããããšãå¿ããªãã§ãã ããã
- ãã¬ã³ããªã³ã°å®äºãâ ãæ£ããåçãããããmoov atomã®äœçœ®ãšãªãŒãã£ãªPTSãä¿®æ£ããããã®FFmpegåŸåŠçãäºç®ã«çµã¿èŸŒã¿ããªãŒãã£ãªãã©ã°ã远å ããåã«ãããŒãããŠãã ããã
- ããã£ãŒãªã³ã°ãã¯æ©èœã§ããã¢ãŒã·ã§ã³å¶çŽãé©å¿åFOVãã¢ãŒã·ã§ã³ãªãŒãã¯ããæè¡çã«ãªãã¬ãŒã ãããããã®ãšããããæ®åœ±ããããã«èŠããããã®ãåãããã®ã§ãã
ãã®çµæïŒè¢«åäœãã¿ãããããã©ããã³ã°ãããããŒãã¬ãŒã ã調æŽããããããªãã©ãããããªããšã¯ã¹ããŒãã§ãããå®å šãªãªã³ããã€ã¹360°ãªãã¬ãŒã ãšãã£ã¿ãŒ â Dartãååã«é«éã«å®è¡ã§ããªãäœæ¥ã®ã¿ãè¡ãèããã€ãã£ãGPUã¬ã€ã€ãŒãæã€Flutterã§æ§ç¯ãããŠããŸãã

