360 Video Streaming Performance on Quest 2 and Quest 3
Hi,
Our team is currently in the process of studying and porting a project previously developed in Unity to the new Meta Spatial SDK. This project involves streaming monoscopic 360° videos (not 3D stereoscopic) on the Quest 2 and Quest 3. I'm using the MediaPlayerSample project available on GitHub as a reference for studying. For video playback, I'm utilizing ExoPlayer for streaming, as VideoView, which was initially used in the project, showed significant performance issues during streaming.
While testing, I noticed the following performance differences:
- On the Quest 2, videos with resolutions starting at 4K and above cause severe performance degradation. The device often freezes, and the application becomes unstable, with frame rates dropping to between 30–50 FPS.
- On the Quest 3, videos up to 5K resolution run perfectly at 90 FPS without any performance issues.
This raises a few questions:
- Is there a way to detect programmatically (in Kotlin) whether the user is using a Quest 2 or Quest 3? Currently, to address performance issues on the Quest 2, I’m dividing the video resolution by 1.75. If I can’t find a solution to improve performance on the Quest 2, I may have to keep this adjustment exclusively for Quest 2 devices.
- Are there specific considerations for implementing shaders (.frag and .vert) for rendering high-resolution 360° videos?
- What is the recommended video format and encoding for streaming monoscopic 360° videos, particularly to optimize performance on the Quest 2?
Below are the shaders I’m currently using. Is there’s anything wrong or missing in my implementation?
Fragment Shader (.frag):
# 360.frag
#version 400
#extension GL_ARB_separate_shader_objects : enable
#extension GL_ARB_shading_language_420pack : enable
#include <common.glsl>
layout (std140, set = 3, binding = 0) uniform MaterialUniform {
vec4 stereoParams;
vec4 customParams;
} g_MaterialUniform;
layout (set = 3, binding = 1) uniform sampler2D albedoSampler;
layout(location = 0) in struct {
vec4 color;
vec2 albedoCoord;
vec2 roughnessMetallicCoord;
vec2 emissiveCoord;
vec2 occlusionCoord;
vec2 normalCoord;
vec3 lighting;
vec3 worldNormal;
vec3 worldPosition;
} vertexOut;
layout (location = 0) out vec4 outColor;
void main() {
vec4 pixel = texture(albedoSampler, vertexOut.albedoCoord);
outColor.rgba = pixel;
}
Vertex Shader (.vert):
# 360.vert
#version 430
#extension GL_ARB_separate_shader_objects : enable
#extension GL_ARB_shading_language_420pack : enable
#include <common.glsl>
#include <app2vertex.glsl>
layout(location = 0) out struct {
vec4 color;
vec2 albedoCoord;
vec2 roughnessMetallicCoord;
vec2 emissiveCoord;
vec2 occlusionCoord;
vec2 normalCoord;
vec3 lighting;
vec3 worldNormal;
vec3 worldPosition;
} vertexOut;
layout (std140, set = 3, binding = 0) uniform MaterialUniform {
vec4 stereoParams;
vec4 customParams;
} g_MaterialUniform;
void main() {
App2VertexUnpacked app = getApp2VertexUnpacked();
vec4 wPos4 = g_PrimitiveUniform.worldFromObject * vec4(app.position, 1.0f);
vertexOut.albedoCoord = app.uv;
vertexOut.lighting = app.incomingLighting;
vertexOut.worldPosition = wPos4.xyz;
vertexOut.worldNormal = normalize((transpose(g_PrimitiveUniform.objectFromWorld) * vec4(app.normal, 0.0f) ).xyz);
gl_Position = getClipFromWorld() * wPos4;
postprocessPosition(gl_Position);
}
Additionally, the video is being rendered using the following configuration:
return PanelRegistration(R.integer.sky_video_id) {
layoutResourceId = R.layout.sky_video_layout
config {
val SPHERE_RADIUS: Float = 300.0f
// Some videos will have other resolutions, this is just a base to test performance.
layoutWidthInPx = 5760
layoutHeightInPx = 2880
pivotOffsetWidth = 0.0f
pivotOffsetHeight = 0.0f
// Do i need stereoMode here ?
stereoMode = StereoMode.MonoUp
sceneMeshCreator = { texture: SceneTexture ->
skyPanelMaterial!!.apply { setTexture("albedoSampler", texture) }
SceneMesh.skybox(SPHERE_RADIUS, skyPanelMaterial!!)
}
}
panel {
setIsVisible(false)
playerView = rootView?.findViewById<PlayerView>(R.id.player_view)
entity?.setComponent(Transform.build { rotateY(180f) })
}
}
Thank you in advance for your help!
Hi!
Thanks for the detailed post! I will break it down and try and hit each part.
Panel Performance
The first thing I will say in reference to panel performance is to utilize layers (https://developers.meta.com/horizon/documentation/spatial-sdk/spatial-sdk-2dpanel-layers). These will allow you to get higher fidelity than regular panels and look better even at lower resolutions.
For the best performance, you can utilize a panel configuration that directly connects your application to the compositor. To enable this in your case, I would make the following changes to your panel registration:
return PanelRegistration(R.integer.sky_video_id) { layoutResourceId = R.layout.sky_video_layout config { layoutWidthInPx = 5760 layoutHeightInPx = 2880 layerConfig = LayerConfig() panelShapeType = PanelShapeType.EQUIRECT radiusForCylinderOrSphere = 300.0f // important! mips = 1 // sceneMeshCreator no longer needed } ... }
You can see we first enabled layers by specifying a `LayerConfig` and we then set the shape of that to the 360 degree content with "equirect". The final thing that allows this optimization to work is the `mips=1`. By default, we generate mipmaps so that panels will not look pixelated when viewing them from far away. If we disable mipmap generation (ie settings mips to 1), we are able to make an optimization and not touch the contents of the panel and it is consumed directly from the compositor.
Using this approach, you should get significantly better performance and quality (I have seen cases where 5K videos go from a stuttering mess to rock solid 90fps)
Using layers, however, comes with the drawback of not being compatible with custom vert/frag shader, since we forward the app directly to the compositor.
Other Questions
> Is there a way to detect programmatically (in Kotlin) whether the user is using a Quest 2 or Quest 3?
It is not recommended to rely too heavily on platform specifics as it may cause incompatibilities with other devices. However, I believe "android.os.Build" should have information that can allow you to distinguish them: https://developer.android.com/reference/android/os/Build
> Are there specific considerations for implementing shaders (.frag and .vert) for rendering high-resolution 360° videos?
The biggest advice is switching to layers. However, we are actively investigating speeding up panel performance in future updates.
> What is the recommended video format and encoding for streaming monoscopic 360° videos, particularly to optimize performance on the Quest 2?
We don't have any specific recommendations as of now (but thanks for the callout!). Tweaking the Exoplayer setting will usually have an impact.
> Do i need stereoMode here ?
If your video is just a single feed of a 360 video, you shouldn't need any stereoMode.
Again, thanks for the detailed feedback! Let me know if you have any other questions!