Skip to content

Commit

Permalink
[SR] Handle orientation change
Browse files Browse the repository at this point in the history
  • Loading branch information
romtsn authored Apr 9, 2024
2 parents f0fcf5d + e9bf0b3 commit a0c2678
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 52 deletions.
8 changes: 5 additions & 3 deletions sentry-android-replay/api/sentry-android-replay.api
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@ public final class io/sentry/android/replay/ReplayCache : java/io/Closeable {
public fun <init> (Lio/sentry/SentryOptions;Lio/sentry/protocol/SentryId;Lio/sentry/android/replay/ScreenshotRecorderConfig;)V
public final fun addFrame (Ljava/io/File;J)V
public fun close ()V
public final fun createVideoOf (JJILjava/io/File;)Lio/sentry/android/replay/GeneratedVideo;
public static synthetic fun createVideoOf$default (Lio/sentry/android/replay/ReplayCache;JJILjava/io/File;ILjava/lang/Object;)Lio/sentry/android/replay/GeneratedVideo;
public final fun createVideoOf (JJIIILjava/io/File;)Lio/sentry/android/replay/GeneratedVideo;
public static synthetic fun createVideoOf$default (Lio/sentry/android/replay/ReplayCache;JJIIILjava/io/File;ILjava/lang/Object;)Lio/sentry/android/replay/GeneratedVideo;
public final fun rotate (J)V
}

public final class io/sentry/android/replay/ReplayIntegration : io/sentry/Integration, io/sentry/ReplayController, io/sentry/android/replay/ScreenshotRecorderCallback, java/io/Closeable {
public final class io/sentry/android/replay/ReplayIntegration : android/content/ComponentCallbacks, io/sentry/Integration, io/sentry/ReplayController, io/sentry/android/replay/ScreenshotRecorderCallback, java/io/Closeable {
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;)V
public fun close ()V
public fun isRecording ()Z
public fun onConfigurationChanged (Landroid/content/res/Configuration;)V
public fun onLowMemory ()V
public fun onScreenshotRecorded (Landroid/graphics/Bitmap;)V
public fun pause ()V
public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,23 @@ public class ReplayCache internal constructor(
private val options: SentryOptions,
private val replayId: SentryId,
private val recorderConfig: ScreenshotRecorderConfig,
private val encoderCreator: (File) -> SimpleVideoEncoder
private val encoderCreator: (videoFile: File, height: Int, width: Int) -> SimpleVideoEncoder
) : Closeable {

public constructor(
options: SentryOptions,
replayId: SentryId,
recorderConfig: ScreenshotRecorderConfig
) : this(options, replayId, recorderConfig, encoderCreator = { videoFile ->
) : this(options, replayId, recorderConfig, encoderCreator = { videoFile, height, width ->
SimpleVideoEncoder(
options,
MuxerConfig(file = videoFile, recorderConfig = recorderConfig)
MuxerConfig(
file = videoFile,
recordingHeight = height,
recordingWidth = width,
frameRate = recorderConfig.frameRate,
bitRate = recorderConfig.bitRate
)
).also { it.start() }
})

Expand Down Expand Up @@ -113,6 +119,10 @@ public class ReplayCache internal constructor(
* @param from desired start of the video represented as unix timestamp in milliseconds
* @param segmentId current segment id, used for inferring the filename to store the
* result video under [replayCacheDir], e.g. "replay_<uuid>/0.mp4", where segmentId=0
* @param height desired height of the video in pixels (e.g. it can change from the initial one
* in case of window resize or orientation change)
* @param width desired width of the video in pixels (e.g. it can change from the initial one
* in case of window resize or orientation change)
* @param videoFile optional, location of the file to store the result video. If this is
* provided, [segmentId] from above is disregarded and not used.
* @return a generated video of type [GeneratedVideo], which contains the resulting video file
Expand All @@ -122,6 +132,8 @@ public class ReplayCache internal constructor(
duration: Long,
from: Long,
segmentId: Int,
height: Int,
width: Int,
videoFile: File = File(replayCacheDir, "$segmentId.mp4")
): GeneratedVideo? {
if (frames.isEmpty()) {
Expand All @@ -133,7 +145,7 @@ public class ReplayCache internal constructor(
}

// TODO: reuse instance of encoder and just change file path to create a different muxer
encoder = synchronized(encoderLock) { encoderCreator(videoFile) }
encoder = synchronized(encoderLock) { encoderCreator(videoFile, height, width) }

val step = 1000 / recorderConfig.frameRate.toLong()
var frameCount = 0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.sentry.android.replay

import android.content.ComponentCallbacks
import android.content.Context
import android.content.res.Configuration
import android.graphics.Bitmap
import android.os.Build
import io.sentry.DateUtils
Expand Down Expand Up @@ -36,12 +38,11 @@ import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference
import kotlin.LazyThreadSafetyMode.NONE

class ReplayIntegration(
private val context: Context,
private val dateProvider: ICurrentDateProvider
) : Integration, Closeable, ScreenshotRecorderCallback, ReplayController {
) : Integration, Closeable, ScreenshotRecorderCallback, ReplayController, ComponentCallbacks {

internal companion object {
private const val TAG = "ReplayIntegration"
Expand All @@ -67,12 +68,7 @@ class ReplayIntegration(
Executors.newSingleThreadScheduledExecutor(ReplayExecutorServiceThreadFactory())
}

private val recorderConfig by lazy(NONE) {
ScreenshotRecorderConfig.from(
context,
options.experimental.sessionReplay
)
}
private lateinit var recorderConfig: ScreenshotRecorderConfig

private fun sample(rate: Double?): Boolean {
if (rate != null) {
Expand Down Expand Up @@ -105,9 +101,15 @@ class ReplayIntegration(
}

this.hub = hub
recorder = WindowRecorder(options, recorderConfig, this)
recorder = WindowRecorder(options, this)
isEnabled.set(true)

try {
context.registerComponentCallbacks(this)
} catch (e: Throwable) {
options.logger.log(INFO, "ComponentCallbacks is not available, orientation changes won't be handled by Session replay", e)
}

addIntegrationToSdkVersion(javaClass)
SentryIntegrationPackageStorage.getInstance()
.addPackage("maven:io.sentry:sentry-android-replay", BuildConfig.VERSION_NAME)
Expand Down Expand Up @@ -148,9 +150,10 @@ class ReplayIntegration(
// tagged with the replay that might never be sent when we're recording in buffer mode
hub?.configureScope { it.replayId = currentReplayId.get() }
}
recorderConfig = ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay)
cache = ReplayCache(options, currentReplayId.get(), recorderConfig)

recorder?.startRecording()
recorder?.startRecording(recorderConfig)
// TODO: replace it with dateProvider.currentTimeMillis to also test it
segmentTimestamp.set(DateUtils.getCurrentDateTime())
replayStartTimestamp.set(dateProvider.currentTimeMillis)
Expand All @@ -172,17 +175,25 @@ class ReplayIntegration(
return
}

if (isFullSession.get()) {
options.logger.log(DEBUG, "Replay is already running in 'session' mode, not capturing for event %s", event.eventId)
if (!(event.isErrored || event.isCrashed)) {
options.logger.log(DEBUG, "Event is not error or crash, not capturing for event %s", event.eventId)
return
}

if (!(event.isErrored || event.isCrashed)) {
options.logger.log(DEBUG, "Event is not error or crash, not capturing for event %s", event.eventId)
val sampled = sample(options.experimental.sessionReplay.errorSampleRate)

// only tag event if it's a session mode or buffer mode that got sampled
if (isFullSession.get() || sampled) {
// don't ask me why
event.setTag("replayId", currentReplayId.get().toString())
}

if (isFullSession.get()) {
options.logger.log(DEBUG, "Replay is already running in 'session' mode, not capturing for event %s", event.eventId)
return
}

if (!sample(options.experimental.sessionReplay.errorSampleRate)) {
if (!sampled) {
options.logger.log(INFO, "Replay wasn't sampled by errorSampleRate, not capturing for event %s", event.eventId)
return
}
Expand All @@ -197,9 +208,11 @@ class ReplayIntegration(
}
val segmentId = currentSegment.get()
val replayId = currentReplayId.get()
val height = recorderConfig.recordingHeight
val width = recorderConfig.recordingWidth
replayExecutor.submitSafely(options, "$TAG.send_replay_for_event") {
val videoDuration =
createAndCaptureSegment(now - currentSegmentTimestamp.time, currentSegmentTimestamp, replayId, segmentId, BUFFER, hint)
createAndCaptureSegment(now - currentSegmentTimestamp.time, currentSegmentTimestamp, replayId, segmentId, height, width, BUFFER, hint)
if (videoDuration != null) {
currentSegment.getAndIncrement()
}
Expand All @@ -209,8 +222,6 @@ class ReplayIntegration(
}

hub?.configureScope { it.replayId = currentReplayId.get() }
// don't ask me why
event.setTag("replayId", currentReplayId.get().toString())
isFullSession.set(true)
}

Expand All @@ -230,9 +241,11 @@ class ReplayIntegration(
val segmentId = currentSegment.get()
val duration = now - currentSegmentTimestamp.time
val replayId = currentReplayId.get()
val height = recorderConfig.recordingHeight
val width = recorderConfig.recordingWidth
replayExecutor.submitSafely(options, "$TAG.pause") {
val videoDuration =
createAndCaptureSegment(duration, currentSegmentTimestamp, replayId, segmentId)
createAndCaptureSegment(duration, currentSegmentTimestamp, replayId, segmentId, height, width)
if (videoDuration != null) {
currentSegment.getAndIncrement()
}
Expand All @@ -250,10 +263,12 @@ class ReplayIntegration(
val duration = now - currentSegmentTimestamp.time
val replayId = currentReplayId.get()
val replayCacheDir = cache?.replayCacheDir
val height = recorderConfig.recordingHeight
val width = recorderConfig.recordingWidth
replayExecutor.submitSafely(options, "$TAG.stop") {
// we don't flush the segment, but we still wanna clean up the folder for buffer mode
if (isFullSession.get()) {
createAndCaptureSegment(duration, currentSegmentTimestamp, replayId, segmentId)
createAndCaptureSegment(duration, currentSegmentTimestamp, replayId, segmentId, height, width)
}
FileUtils.deleteRecursively(replayCacheDir)
}
Expand All @@ -272,6 +287,8 @@ class ReplayIntegration(
// have to do it before submitting, otherwise if the queue is busy, the timestamp won't be
// reflecting the exact time of when it was captured
val frameTimestamp = dateProvider.currentTimeMillis
val height = recorderConfig.recordingHeight
val width = recorderConfig.recordingWidth
replayExecutor.submitSafely(options, "$TAG.add_frame") {
cache?.addFrame(bitmap, frameTimestamp)

Expand All @@ -288,7 +305,9 @@ class ReplayIntegration(
options.experimental.sessionReplay.sessionSegmentDuration,
currentSegmentTimestamp,
replayId,
segmentId
segmentId,
height,
width
)
if (videoDuration != null) {
currentSegment.getAndIncrement()
Expand All @@ -311,13 +330,17 @@ class ReplayIntegration(
currentSegmentTimestamp: Date,
replayId: SentryId,
segmentId: Int,
height: Int,
width: Int,
replayType: ReplayType = SESSION,
hint: Hint? = null
): Long? {
val generatedVideo = cache?.createVideoOf(
duration,
currentSegmentTimestamp.time,
segmentId
segmentId,
height,
width
) ?: return null

val (video, frameCount, videoDuration) = generatedVideo
Expand All @@ -326,6 +349,8 @@ class ReplayIntegration(
replayId,
currentSegmentTimestamp,
segmentId,
height,
width,
frameCount,
videoDuration,
replayType,
Expand All @@ -339,6 +364,8 @@ class ReplayIntegration(
currentReplayId: SentryId,
segmentTimestamp: Date,
segmentId: Int,
height: Int,
width: Int,
frameCount: Int,
duration: Long,
replayType: ReplayType,
Expand All @@ -361,8 +388,8 @@ class ReplayIntegration(
payload = listOf(
RRWebMetaEvent().apply {
this.timestamp = segmentTimestamp.time
height = recorderConfig.recordingHeight
width = recorderConfig.recordingWidth
this.height = height
this.width = width
},
RRWebVideoEvent().apply {
this.timestamp = segmentTimestamp.time
Expand All @@ -371,8 +398,8 @@ class ReplayIntegration(
this.frameCount = frameCount
size = video.length()
frameRate = recorderConfig.frameRate
height = recorderConfig.recordingHeight
width = recorderConfig.recordingWidth
this.height = height
this.width = width
// TODO: support non-fullscreen windows later
left = 0
top = 0
Expand All @@ -388,10 +415,46 @@ class ReplayIntegration(
return
}

try {
context.unregisterComponentCallbacks(this)
} catch (ignored: Throwable) {
}
stop()
replayExecutor.gracefullyShutdown(options)
}

override fun onConfigurationChanged(newConfig: Configuration) {
if (!isEnabled.get()) {
return
}

recorder?.stopRecording()

// TODO: support buffer mode and breadcrumb/rrweb_event
if (isFullSession.get()) {
val now = dateProvider.currentTimeMillis
val currentSegmentTimestamp = segmentTimestamp.get()
val segmentId = currentSegment.get()
val duration = now - currentSegmentTimestamp.time
val replayId = currentReplayId.get()
val height = recorderConfig.recordingHeight
val width = recorderConfig.recordingWidth
replayExecutor.submitSafely(options, "$TAG.onConfigurationChanged") {
val videoDuration =
createAndCaptureSegment(duration, currentSegmentTimestamp, replayId, segmentId, height, width)
if (videoDuration != null) {
currentSegment.getAndIncrement()
}
}
}

// refresh config based on new device configuration
recorderConfig = ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay)
recorder?.startRecording(recorderConfig)
}

override fun onLowMemory() = Unit

private class ReplayExecutorServiceThreadFactory : ThreadFactory {
private var cnt = 0
override fun newThread(r: Runnable): Thread {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import kotlin.LazyThreadSafetyMode.NONE
@TargetApi(26)
internal class WindowRecorder(
private val options: SentryOptions,
private val recorderConfig: ScreenshotRecorderConfig,
private val screenshotRecorderCallback: ScreenshotRecorderCallback
) : Closeable {

Expand Down Expand Up @@ -50,7 +49,7 @@ internal class WindowRecorder(
}
}

fun startRecording() {
fun startRecording(recorderConfig: ScreenshotRecorderConfig) {
if (isRecording.getAndSet(true)) {
return
}
Expand Down
Loading

0 comments on commit a0c2678

Please sign in to comment.