@whereby.com/browser-sdk 2.1.0-beta1 → 2.1.0-beta2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8690,7 +8690,7 @@ const selectRoomConnectionState = createSelector(selectChatMessages, selectCloud
8690
8690
  return state;
8691
8691
  });
8692
8692
 
8693
- const sdkVersion = "2.1.0-beta1";
8693
+ const sdkVersion = "2.1.0-beta2";
8694
8694
 
8695
8695
  const initialState$1 = {
8696
8696
  chatMessages: [],
@@ -8842,4 +8842,784 @@ function useLocalMedia(optionsOrStream = { audio: true, video: true }) {
8842
8842
  };
8843
8843
  }
8844
8844
 
8845
- export { VideoView, sdkVersion, useLocalMedia, useRoomConnection };
8845
+ // Source of truth layout related constants
8846
+ const VIDEO_CONTROLS_MIN_WIDTH$1 = 7 * 60;
8847
+ var layoutConstants = {
8848
+ // Minimum window size before we start floating the toolbars
8849
+ MIN_WINDOW_HEIGHT: 320,
8850
+ MIN_WINDOW_WIDTH: 320,
8851
+ // Breakpoints
8852
+ DESKTOP_BREAKPOINT: 1025,
8853
+ TABLET_BREAKPOINT: 750,
8854
+ PHONE_BREAKPOINT: 500,
8855
+ // Room layout
8856
+ TOP_TOOLBAR_HEIGHT: 40 + 8 * 2,
8857
+ BOTTOM_TOOLBAR_HEIGHT: 70 + 4 * 3,
8858
+ SIDEBAR_WIDTH: 375,
8859
+ VIDEO_CONTROLS_MIN_WIDTH: VIDEO_CONTROLS_MIN_WIDTH$1,
8860
+ ROOM_FOOTER_MIN_WIDTH: 60 * 3 + VIDEO_CONTROLS_MIN_WIDTH$1,
8861
+ FLOATING_VIDEO_CONTROLS_BOTTOM_MARGIN: 20,
8862
+ WATERMARK_BAR_HEIGHT: 32,
8863
+ // Breakout stage (no active group)
8864
+ BREAKOUT_STAGE_BACKDROP_HEADER_HEIGHT: 20 + 8,
8865
+ BREAKOUT_STAGE_BACKDROP_FOOTER_HEIGHT: 8 + 40 + 8,
8866
+ // Subgrid
8867
+ SUBGRID_EMPTY_STAGE_MAX_WIDTH: 800,
8868
+ // Groups grid
8869
+ GROUPS_CELL_MARGIN: 8,
8870
+ GROUPS_CELL_PADDING: 12,
8871
+ GROUPS_CELL_NAV_HEIGHT: 48 + 8,
8872
+ GROUPS_CELL_AVATAR_WRAPPER_BOTTOM_MARGIN: 8,
8873
+ GROUPS_CELL_AVATAR_GRID_GAP: 8,
8874
+ GROUPS_CELL_MIN_WIDTH: 360,
8875
+ GROUPS_CELL_MAX_WIDTH: 600,
8876
+ // Groups table
8877
+ GROUPS_ROW_HEIGHT: 72,
8878
+ GROUPS_ROW_GAP: 1,
8879
+ // Foldable screen
8880
+ FOLDABLE_SCREEN_STAGE_PADDING: 8,
8881
+ };
8882
+
8883
+ function makeOrigin({ top = 0, left = 0 } = {}) {
8884
+ return {
8885
+ top,
8886
+ left,
8887
+ };
8888
+ }
8889
+ function makeBounds({ width = 0, height = 0 } = {}) {
8890
+ return {
8891
+ width: Math.max(width, 0),
8892
+ height: Math.max(height, 0),
8893
+ };
8894
+ }
8895
+ function makeFrame({ top = 0, left = 0, width = 0, height = 0 } = {}) {
8896
+ return {
8897
+ bounds: makeBounds({ width, height }),
8898
+ origin: makeOrigin({ top, left }),
8899
+ };
8900
+ }
8901
+ function makeBox({ top = 0, left = 0, bottom = 0, right = 0 } = {}) {
8902
+ return {
8903
+ top,
8904
+ left,
8905
+ bottom,
8906
+ right,
8907
+ };
8908
+ }
8909
+
8910
+ function fitToBounds(aspectRatio, containerSize) {
8911
+ const { width, height } = containerSize;
8912
+ const contentHeight = height;
8913
+ const contentWidth = contentHeight * aspectRatio;
8914
+ const scale = Math.min(width / contentWidth, height / contentHeight);
8915
+ const adjustedWidth = contentWidth * scale;
8916
+ const adjustedHeight = contentHeight * scale;
8917
+ return { width: adjustedWidth, height: adjustedHeight };
8918
+ }
8919
+ const cellContentArea = ({ width, height, rows, cols, aspectRatio, }) => {
8920
+ const bounds = fitToBounds(aspectRatio, { width: width / cols, height: height / rows });
8921
+ return Math.round(bounds.width * bounds.height);
8922
+ };
8923
+ const getWeightedSplitCount = ({ vertical, width, height, count, aspectRatio, }) => {
8924
+ // Calculate cell content areas for 1, 2 and 3 (columns|rows) layouts
8925
+ // and pick the largest one:
8926
+ const choices = [1, 2, 3].map((rowCols) => cellContentArea({
8927
+ width,
8928
+ height,
8929
+ rows: vertical ? Math.ceil(count / rowCols) : rowCols,
8930
+ cols: vertical ? rowCols : Math.ceil(count / rowCols),
8931
+ aspectRatio,
8932
+ }));
8933
+ const closest = Math.max(...choices);
8934
+ const splits = choices.indexOf(closest) + 1;
8935
+ return { splits, weight: closest };
8936
+ };
8937
+ const getGridSplits = ({ width, height, count, aspectRatio, }) => {
8938
+ // Try both vertical and horizontal layout and pick the one that yields the
8939
+ // biggest video cells:
8940
+ const verticalPick = getWeightedSplitCount({ vertical: true, width, height, count, aspectRatio });
8941
+ const horizontalPick = getWeightedSplitCount({ vertical: false, width, height, count, aspectRatio });
8942
+ if (verticalPick.weight > horizontalPick.weight) {
8943
+ return { splits: verticalPick.splits, vertical: true };
8944
+ }
8945
+ return { splits: horizontalPick.splits, vertical: false };
8946
+ };
8947
+ function getGridSizeForCount({ count, width, height, aspectRatio, }) {
8948
+ if (count <= 1) {
8949
+ return {
8950
+ rows: 1,
8951
+ cols: 1,
8952
+ };
8953
+ }
8954
+ const { splits, vertical } = getGridSplits({ width, height, count, aspectRatio });
8955
+ if (vertical) {
8956
+ return {
8957
+ rows: Math.ceil(count / splits),
8958
+ cols: splits,
8959
+ };
8960
+ }
8961
+ return {
8962
+ rows: splits,
8963
+ cols: Math.ceil(count / splits),
8964
+ };
8965
+ }
8966
+
8967
+ const WIDE_AR = 16 / 9;
8968
+ const NORMAL_AR = 4 / 3;
8969
+ const clamp = ({ value, min, max }) => Math.min(Math.max(value, min), max);
8970
+ function hasDuplicates(...array) {
8971
+ return new Set(array).size !== array.length;
8972
+ }
8973
+ function findMostCommon(arr) {
8974
+ return arr.sort((a, b) => arr.filter((v) => v === a).length - arr.filter((v) => v === b).length).pop();
8975
+ }
8976
+ // Grid cells are all the same aspect ratio (not to be confused with the video cells)
8977
+ // Pick the best ratio given a list of the video cell ratios:
8978
+ function pickCellAspectRatio({ choices = [] }) {
8979
+ // If all cells are the same aspect ratio use that:
8980
+ const minAr = Math.min(...choices);
8981
+ const maxAr = Math.max(...choices);
8982
+ let chosenAr = null;
8983
+ if (minAr === maxAr) {
8984
+ chosenAr = minAr;
8985
+ }
8986
+ else {
8987
+ // Otherwise we're in a mixed grid.
8988
+ // We ideally want to make the majority ratio look nice. Pick the most common
8989
+ // ratio but limit it to wide cells. If we don't have a majority choice
8990
+ // just go with the widest:
8991
+ const dominantAr = hasDuplicates(choices) ? findMostCommon(choices) : maxAr;
8992
+ chosenAr = clamp({ value: dominantAr || maxAr, min: NORMAL_AR, max: WIDE_AR });
8993
+ }
8994
+ return {
8995
+ minAr,
8996
+ maxAr,
8997
+ chosenAr,
8998
+ };
8999
+ }
9000
+ // Calculate how much we need to move the last row horizontally so it
9001
+ // becomes centered:
9002
+ function getCenterPadding({ rows, cols, cellWidth, index, cellCount, gridGap, }) {
9003
+ const max = rows * cols;
9004
+ const leftOver = max - cellCount;
9005
+ if (!leftOver) {
9006
+ return 0;
9007
+ }
9008
+ const lastIndex = max - leftOver - 1;
9009
+ const firstIndex = lastIndex - (cols - leftOver) + 1;
9010
+ const lastRowPadding = (leftOver * cellWidth) / 2 + gridGap;
9011
+ return index >= firstIndex && index <= lastIndex ? lastRowPadding : 0;
9012
+ }
9013
+ function getCellBounds({ width, height, rows, cols, gridGap, aspectRatio, }) {
9014
+ // Naively calculate the cell size based on grid and container size:
9015
+ const cellWidth = (width - (cols - 1) * gridGap) / cols;
9016
+ const cellHeight = (height - (rows - 1) * gridGap) / rows;
9017
+ const ar = cellWidth / cellHeight;
9018
+ // Knowing the target cell aspect ratio, pull any extra space
9019
+ // into the grid padding:
9020
+ let horizontalCorrection = 0;
9021
+ let verticalCorrection = 0;
9022
+ if (aspectRatio < ar) {
9023
+ horizontalCorrection = cellWidth - cellHeight * aspectRatio;
9024
+ }
9025
+ else if (aspectRatio > ar) {
9026
+ verticalCorrection = cellHeight - cellWidth / aspectRatio;
9027
+ }
9028
+ const totalHorizontalCorrection = horizontalCorrection * cols;
9029
+ const totalVerticalCorrection = verticalCorrection * rows;
9030
+ return {
9031
+ cellWidth: cellWidth - horizontalCorrection,
9032
+ cellHeight: cellHeight - verticalCorrection,
9033
+ extraHorizontalPadding: totalHorizontalCorrection / 2,
9034
+ extraVerticalPadding: totalVerticalCorrection / 2,
9035
+ };
9036
+ }
9037
+ function calculateLayout$1({ width, height, cellCount, gridGap, cellAspectRatios = [NORMAL_AR], paddings = makeBox(), }) {
9038
+ // Handle empty grid:
9039
+ if (!cellCount) {
9040
+ return {
9041
+ cellCount,
9042
+ cellHeight: 0,
9043
+ cellWidth: 0,
9044
+ cols: 0,
9045
+ rows: 0,
9046
+ extraHorizontalPadding: 0,
9047
+ extraVerticalPadding: 0,
9048
+ gridGap,
9049
+ paddings,
9050
+ };
9051
+ }
9052
+ const contentWidth = width - (paddings.left + paddings.right);
9053
+ const contentHeight = height - (paddings.top + paddings.bottom);
9054
+ const cellAspectRatioTuple = pickCellAspectRatio({
9055
+ choices: cellAspectRatios,
9056
+ });
9057
+ let cellAspectRatio = cellAspectRatioTuple.chosenAr;
9058
+ const { rows, cols } = getGridSizeForCount({
9059
+ count: cellCount,
9060
+ width: contentWidth,
9061
+ height: contentHeight,
9062
+ aspectRatio: cellAspectRatio,
9063
+ });
9064
+ // Special case 1 col / row:
9065
+ // Divvy up available all space (within reason)
9066
+ if (rows === 1) {
9067
+ cellAspectRatio = clamp({
9068
+ value: contentWidth / cols / contentHeight,
9069
+ min: Math.min(cellAspectRatioTuple.chosenAr, cellAspectRatioTuple.maxAr),
9070
+ max: Math.max(cellAspectRatioTuple.chosenAr, cellAspectRatioTuple.maxAr),
9071
+ });
9072
+ }
9073
+ else if (cols === 1) {
9074
+ cellAspectRatio = clamp({
9075
+ value: contentWidth / (contentHeight / rows),
9076
+ min: Math.min(cellAspectRatioTuple.chosenAr, cellAspectRatioTuple.maxAr),
9077
+ max: Math.max(cellAspectRatioTuple.chosenAr, cellAspectRatioTuple.maxAr),
9078
+ });
9079
+ }
9080
+ const { cellWidth, cellHeight, extraHorizontalPadding, extraVerticalPadding } = getCellBounds({
9081
+ width: contentWidth,
9082
+ height: contentHeight,
9083
+ rows,
9084
+ cols,
9085
+ gridGap,
9086
+ aspectRatio: cellAspectRatio,
9087
+ });
9088
+ return {
9089
+ cellCount,
9090
+ cellHeight,
9091
+ cellWidth,
9092
+ cols,
9093
+ rows,
9094
+ extraHorizontalPadding,
9095
+ extraVerticalPadding,
9096
+ // pass through
9097
+ gridGap,
9098
+ paddings,
9099
+ };
9100
+ }
9101
+ function getCellPropsAtIndexForLayout({ index, layout, }) {
9102
+ const { cellWidth, cellHeight, rows, cols, cellCount, gridGap } = layout;
9103
+ const top = Math.floor(index / cols);
9104
+ const left = Math.floor(index % cols);
9105
+ const leftPadding = getCenterPadding({ rows, cols, cellWidth, index, cellCount, gridGap });
9106
+ return {
9107
+ top: top * cellHeight + top * gridGap,
9108
+ left: left * cellWidth + left * gridGap + leftPadding,
9109
+ width: cellWidth,
9110
+ height: cellHeight,
9111
+ };
9112
+ }
9113
+
9114
+ const { BOTTOM_TOOLBAR_HEIGHT, VIDEO_CONTROLS_MIN_WIDTH, TABLET_BREAKPOINT } = layoutConstants;
9115
+ const MIN_GRID_HEIGHT = 200;
9116
+ const MIN_GRID_WIDTH = 300;
9117
+ const FLOATING_VIDEO_SIZE = 200;
9118
+ const CONSTRAINED_OVERFLOW_TRIGGER = 12;
9119
+ function getMinGridBounds({ cellCount }) {
9120
+ // Reduce min grid dimensions if we have 6 videos or less
9121
+ const isSmallGrid = cellCount <= 6;
9122
+ const minGridHeight = isSmallGrid ? MIN_GRID_HEIGHT - 50 : MIN_GRID_HEIGHT;
9123
+ const minGridWidth = isSmallGrid ? MIN_GRID_WIDTH - 50 : MIN_GRID_WIDTH;
9124
+ return makeBounds({ width: minGridWidth, height: minGridHeight });
9125
+ }
9126
+ function fitSupersizedContent({ bounds, aspectRatio, minGridContainerBounds, hasPresentationGrid, }) {
9127
+ const { width, height } = bounds;
9128
+ // If we don't have any grids take up whole stage
9129
+ const hasVideoGrid = minGridContainerBounds.width > 0;
9130
+ if (!hasVideoGrid) {
9131
+ return {
9132
+ isPortrait: width <= height,
9133
+ supersizedContentBounds: bounds,
9134
+ };
9135
+ }
9136
+ // Calculate minimum supersized content bounds - take up at least half the
9137
+ // available area:
9138
+ const minHorizontalSupersizedContentWidth = Math.round(width / 2);
9139
+ const minVerticalSupersizedContentHeight = Math.round(height / 2);
9140
+ // Calculate maximum supersized content bounds
9141
+ const maxHorizontalSupersizedContentWidth = Math.max(width - minGridContainerBounds.width, 0);
9142
+ const maxVerticalSupersizedContentHeight = Math.max(height - minGridContainerBounds.height, 0);
9143
+ let isPortrait = maxHorizontalSupersizedContentWidth <= maxVerticalSupersizedContentHeight;
9144
+ let horizontalCorrection = 0;
9145
+ let verticalCorrection = 0;
9146
+ // Do we have an aspect ratio? If not give up all available space (ex some integrations)
9147
+ if (aspectRatio) {
9148
+ // Calculate fit bounds for both portrait and landscape layouts:
9149
+ // 1. grid to the left of content
9150
+ const horizontalContentBounds = fitToBounds(aspectRatio, {
9151
+ width: maxHorizontalSupersizedContentWidth,
9152
+ height,
9153
+ });
9154
+ // 2. grid below content
9155
+ const verticalContentBounds = fitToBounds(aspectRatio, {
9156
+ width,
9157
+ height: maxVerticalSupersizedContentHeight,
9158
+ });
9159
+ // Pick direction that gives content most space:
9160
+ const isPortraitContent = aspectRatio <= 1.0;
9161
+ isPortrait = isPortraitContent
9162
+ ? verticalContentBounds.height > horizontalContentBounds.height
9163
+ : verticalContentBounds.width > horizontalContentBounds.width;
9164
+ // Give wasted space back to the video grid:
9165
+ if (isPortrait) {
9166
+ const wastedSpace = maxVerticalSupersizedContentHeight -
9167
+ Math.max(verticalContentBounds.height, minVerticalSupersizedContentHeight);
9168
+ verticalCorrection = Math.max(wastedSpace, 0);
9169
+ }
9170
+ else {
9171
+ const wastedSpace = maxHorizontalSupersizedContentWidth -
9172
+ Math.max(horizontalContentBounds.width, minHorizontalSupersizedContentWidth);
9173
+ horizontalCorrection = Math.max(wastedSpace, 0);
9174
+ }
9175
+ }
9176
+ else if (hasPresentationGrid) {
9177
+ // If we have more than one presentation grid cell we naively favor portrait orientation
9178
+ // unless it gets too squished:
9179
+ isPortrait = maxHorizontalSupersizedContentWidth / maxVerticalSupersizedContentHeight >= 5;
9180
+ }
9181
+ const supersizedContentBounds = {
9182
+ width: isPortrait ? width : maxHorizontalSupersizedContentWidth - horizontalCorrection,
9183
+ height: isPortrait ? maxVerticalSupersizedContentHeight - verticalCorrection : height,
9184
+ };
9185
+ return {
9186
+ isPortrait,
9187
+ supersizedContentBounds,
9188
+ };
9189
+ }
9190
+ // The stage layout is the base room layout
9191
+ // It divides the stage area between a videos container (made up of video grid +
9192
+ // presentation grid)
9193
+ function calculateStageLayout({ containerBounds, containerOrigin, hasConstrainedOverflow, hasPresentationContent, hasVideoContent, isPortrait, }) {
9194
+ const hasVideos = hasPresentationContent || hasVideoContent;
9195
+ // Sanity checks
9196
+ // Do we have anything to calculate?
9197
+ if (!hasVideos) {
9198
+ return {
9199
+ isPortrait,
9200
+ videosContainer: makeFrame(),
9201
+ hasOverflow: false,
9202
+ };
9203
+ }
9204
+ return {
9205
+ isPortrait,
9206
+ videosContainer: makeFrame(Object.assign(Object.assign({}, containerBounds), containerOrigin)),
9207
+ hasOverflow: hasConstrainedOverflow,
9208
+ };
9209
+ }
9210
+ function calculateVideosContainerLayout({ containerBounds, containerOrigin, gridGap, supersizedContentAspectRatio, hasPresentationContent, hasPresentationGrid, hasVideoContent, minGridBounds, }) {
9211
+ const { width, height } = containerBounds;
9212
+ let isPortrait = width <= height;
9213
+ let presentationGridBounds = makeBounds();
9214
+ let presentationGridOrigin = makeOrigin();
9215
+ let videoGridBounds = hasVideoContent ? Object.assign({}, containerBounds) : makeBounds();
9216
+ let videoGridOrigin = hasVideoContent ? Object.assign({}, containerOrigin) : makeOrigin();
9217
+ if (hasPresentationContent) {
9218
+ // Fit supersized content
9219
+ const minGridContainerBounds = makeBounds({
9220
+ width: hasVideoContent ? minGridBounds.width + gridGap : 0,
9221
+ height: hasVideoContent ? minGridBounds.height + gridGap : 0,
9222
+ });
9223
+ const supersizedContentLayout = fitSupersizedContent({
9224
+ bounds: containerBounds,
9225
+ aspectRatio: supersizedContentAspectRatio,
9226
+ minGridContainerBounds,
9227
+ hasPresentationGrid,
9228
+ });
9229
+ isPortrait = supersizedContentLayout.isPortrait;
9230
+ presentationGridBounds = supersizedContentLayout.supersizedContentBounds;
9231
+ presentationGridOrigin = Object.assign({}, containerOrigin);
9232
+ if (hasVideoContent) {
9233
+ videoGridBounds = makeBounds({
9234
+ width: isPortrait
9235
+ ? containerBounds.width
9236
+ : containerBounds.width - presentationGridBounds.width - gridGap,
9237
+ height: isPortrait
9238
+ ? containerBounds.height - presentationGridBounds.height - gridGap
9239
+ : containerBounds.height,
9240
+ });
9241
+ videoGridOrigin = makeOrigin({
9242
+ top: isPortrait ? containerOrigin.top + presentationGridBounds.height + gridGap : containerOrigin.top,
9243
+ left: isPortrait ? containerOrigin.left : containerOrigin.left + presentationGridBounds.width + gridGap,
9244
+ });
9245
+ }
9246
+ }
9247
+ return {
9248
+ isPortrait,
9249
+ presentationGrid: Object.assign({}, makeFrame(Object.assign(Object.assign({}, presentationGridBounds), presentationGridOrigin))),
9250
+ videoGrid: makeFrame(Object.assign(Object.assign({}, videoGridBounds), videoGridOrigin)),
9251
+ };
9252
+ }
9253
+ function calculateGridLayout({ containerBounds, paddings = makeBox(), videos, isConstrained, maxGridWidth, gridGap, }) {
9254
+ const { width, height } = containerBounds;
9255
+ const cappedWidth = maxGridWidth ? Math.min(width, maxGridWidth) : width;
9256
+ const cellCount = videos.length;
9257
+ let videoCells = null;
9258
+ const cellAspectRatios = videos.map((video) => video.aspectRatio);
9259
+ const minGridBounds = getMinGridBounds({ cellCount });
9260
+ // Cap grid to a sane width (on very wide monitors)
9261
+ const gridLayout = calculateLayout$1({
9262
+ width: cappedWidth,
9263
+ height,
9264
+ cellCount,
9265
+ gridGap,
9266
+ cellAspectRatios,
9267
+ paddings,
9268
+ });
9269
+ videoCells = videos.map((video, index) => {
9270
+ const cellProps = getCellPropsAtIndexForLayout({ index, layout: gridLayout });
9271
+ const isSmallCell = gridLayout.cellWidth < minGridBounds.width;
9272
+ const shouldZoom = isConstrained || isSmallCell;
9273
+ const aspectRatio = shouldZoom ? gridLayout.cellWidth / gridLayout.cellHeight : video.aspectRatio;
9274
+ return {
9275
+ clientId: video.clientId,
9276
+ isDraggable: video.isDraggable,
9277
+ origin: makeOrigin({
9278
+ top: cellProps.top,
9279
+ left: cellProps.left,
9280
+ }),
9281
+ bounds: makeBounds({
9282
+ width: cellProps.width,
9283
+ height: cellProps.height,
9284
+ }),
9285
+ aspectRatio,
9286
+ isSmallCell,
9287
+ };
9288
+ });
9289
+ return {
9290
+ videoCells,
9291
+ extraHorizontalPadding:
9292
+ // If we hit the max width, pass up as extra space
9293
+ width !== cappedWidth
9294
+ ? gridLayout.extraHorizontalPadding + (width - cappedWidth) / 2
9295
+ : gridLayout.extraHorizontalPadding,
9296
+ extraVerticalPadding: gridLayout.extraVerticalPadding,
9297
+ paddings: gridLayout.paddings,
9298
+ gridGap,
9299
+ };
9300
+ }
9301
+ function calculateFloatingLayout({ roomBounds, containerFrame, floatingVideo, videoControlsHeight, margin = 8, }) {
9302
+ if (!floatingVideo) {
9303
+ return null;
9304
+ }
9305
+ const bounds = fitToBounds(floatingVideo.aspectRatio, {
9306
+ width: FLOATING_VIDEO_SIZE,
9307
+ height: FLOATING_VIDEO_SIZE,
9308
+ });
9309
+ // Determine if we should position above the video controls or not
9310
+ const isFloating = !(roomBounds.height - containerFrame.bounds.height - containerFrame.origin.top);
9311
+ const isConstrained = containerFrame.bounds.width - (bounds.width + margin) * 2 < VIDEO_CONTROLS_MIN_WIDTH;
9312
+ let verticalOffset = 0;
9313
+ if (isFloating && isConstrained) {
9314
+ // Pull up above floating video controls
9315
+ verticalOffset = videoControlsHeight * -1;
9316
+ }
9317
+ else if (!isFloating && !isConstrained) {
9318
+ // Push down over the bottom toolbar
9319
+ verticalOffset = videoControlsHeight;
9320
+ }
9321
+ const origin = makeOrigin({
9322
+ top: containerFrame.origin.top + (containerFrame.bounds.height - bounds.height - margin) + verticalOffset,
9323
+ left: containerFrame.origin.left + (containerFrame.bounds.width - bounds.width - margin),
9324
+ });
9325
+ const videoCell = {
9326
+ clientId: floatingVideo.clientId,
9327
+ isDraggable: floatingVideo.isDraggable,
9328
+ origin,
9329
+ bounds,
9330
+ aspectRatio: floatingVideo.aspectRatio,
9331
+ isSmallCell: true,
9332
+ };
9333
+ return videoCell;
9334
+ }
9335
+ function rebalanceLayoutPaddedAreas({ a, b, gridGap, isPortrait, }) {
9336
+ const aPad = isPortrait ? a.vertical : a.horizontal;
9337
+ const bPad = isPortrait ? b.vertical : b.horizontal;
9338
+ if (aPad === bPad) {
9339
+ return { a: 0, b: 0 };
9340
+ }
9341
+ const sArea = aPad < bPad ? a : b;
9342
+ const sAreaPad = isPortrait ? sArea.vertical : sArea.horizontal;
9343
+ const spaceBetween = gridGap + (aPad + bPad);
9344
+ const offset = (spaceBetween + sAreaPad) / 2 - sAreaPad;
9345
+ return {
9346
+ a: sArea === a ? offset : 0,
9347
+ b: sArea === b ? offset : 0,
9348
+ };
9349
+ }
9350
+ function rebalanceLayoutInPlace({ videosContainerLayout, gridLayout, presentationGridLayout, gridGap, }) {
9351
+ const hasPresentationGrid = videosContainerLayout.presentationGrid.bounds.width > 0;
9352
+ const hasVideoGrid = videosContainerLayout.videoGrid.bounds.width > 0;
9353
+ // Rebalance video containers if we have both presentationGrid and videoGrid bounds,
9354
+ // unless we have a breakout no group stage:
9355
+ if (hasPresentationGrid && hasVideoGrid) {
9356
+ const correction = rebalanceLayoutPaddedAreas({
9357
+ a: {
9358
+ horizontal: presentationGridLayout.extraHorizontalPadding,
9359
+ vertical: presentationGridLayout.extraVerticalPadding,
9360
+ },
9361
+ b: {
9362
+ horizontal: gridLayout.extraHorizontalPadding,
9363
+ vertical: gridLayout.extraVerticalPadding,
9364
+ },
9365
+ gridGap,
9366
+ isPortrait: videosContainerLayout.isPortrait,
9367
+ });
9368
+ // Update in place:
9369
+ if (videosContainerLayout.isPortrait) {
9370
+ videosContainerLayout.presentationGrid.origin.top += correction.a;
9371
+ videosContainerLayout.videoGrid.origin.top -= correction.b;
9372
+ // Save off how much we moved the grid over to be used in the next phase:
9373
+ correction.b;
9374
+ }
9375
+ else {
9376
+ videosContainerLayout.presentationGrid.origin.left += correction.a;
9377
+ videosContainerLayout.videoGrid.origin.left -= correction.b;
9378
+ // Save off how much we moved the grid over to be used in the next phase:
9379
+ correction.b;
9380
+ }
9381
+ }
9382
+ }
9383
+ function calculateGridLayouts({ gridGap, isConstrained, presentationVideos, videos, videosContainerLayout, gridLayoutPaddings = makeBox(), presentationGridLayoutPaddings = makeBox(), maxGridWidth, }) {
9384
+ // Lay out video cells in provided video containers:
9385
+ const gridLayout = calculateGridLayout({
9386
+ containerBounds: videosContainerLayout.videoGrid.bounds,
9387
+ gridGap,
9388
+ isConstrained,
9389
+ maxGridWidth,
9390
+ paddings: gridLayoutPaddings,
9391
+ videos,
9392
+ });
9393
+ const presentationGridLayout = calculateGridLayout({
9394
+ containerBounds: videosContainerLayout.presentationGrid.bounds,
9395
+ gridGap,
9396
+ isConstrained,
9397
+ maxGridWidth,
9398
+ paddings: presentationGridLayoutPaddings,
9399
+ videos: presentationVideos,
9400
+ });
9401
+ return { gridLayout, presentationGridLayout };
9402
+ }
9403
+ function calculateLayout({ floatingVideo = null, frame, gridGap = 0, isConstrained = false, isMaximizeMode = false, paddings = makeBox(), presentationVideos = [], rebalanceLayout = false, roomBounds, roomLayoutHasOverlow = false, videoControlsHeight = 0, videos = [], videoGridGap = 0, }) {
9404
+ const hasPresentationContent = !!presentationVideos.length;
9405
+ const hasPresentationGrid = presentationVideos.length > 1;
9406
+ const supersizedContentAspectRatio = hasPresentationContent && !hasPresentationGrid ? presentationVideos[0].aspectRatio : 1;
9407
+ const hasVideoContent = !!videos.length;
9408
+ const width = frame.bounds.width - paddings.left - paddings.right;
9409
+ let height = frame.bounds.height - paddings.top - paddings.bottom;
9410
+ const maxGridWidth = Math.max(25 * 88, (80 / 100) * width); // go up to 80vw after a sane max width
9411
+ // On mobile, we set a hard limit on 12 videos, and overflows after that.
9412
+ const hasConstrainedOverflow = (isConstrained && videos.length > CONSTRAINED_OVERFLOW_TRIGGER) || false;
9413
+ const lineHeight = height / 4;
9414
+ const extraLines = Math.ceil((videos.length - CONSTRAINED_OVERFLOW_TRIGGER) / 3);
9415
+ height = hasConstrainedOverflow ? height + lineHeight * extraLines : height;
9416
+ const stageBounds = makeBounds({ width, height });
9417
+ const stageOrigin = makeOrigin({ top: paddings.top, left: paddings.left });
9418
+ const _minBounds = getMinGridBounds({ cellCount: videos.length });
9419
+ const minGridBounds = _minBounds;
9420
+ const isSmallScreen = roomBounds.width < TABLET_BREAKPOINT || roomBounds.height < TABLET_BREAKPOINT;
9421
+ const forceStageLayoutPortrait = isMaximizeMode;
9422
+ const stageLayoutIsPortrait = forceStageLayoutPortrait ||
9423
+ !(hasPresentationContent || hasVideoContent) ||
9424
+ stageBounds.width <= stageBounds.height;
9425
+ const stableStageLayoutProps = {
9426
+ cellPaddings: { top: 4, left: 4, bottom: 4, right: 4 },
9427
+ containerBounds: stageBounds,
9428
+ containerOrigin: stageOrigin,
9429
+ gridGap,
9430
+ hasPresentationContent,
9431
+ hasVideoContent,
9432
+ isConstrained,
9433
+ isMaximizeMode,
9434
+ isSmallScreen,
9435
+ maxGridWidth,
9436
+ };
9437
+ let stageLayout = calculateStageLayout(Object.assign(Object.assign({}, stableStageLayoutProps), { isPortrait: stageLayoutIsPortrait, hasConstrainedOverflow }));
9438
+ // Prevent yo-yo-ing between overflow and non overflow states:
9439
+ // - if we're not in a forced overflow state and main room layout has overflow already (prev we could not fit) and now we can fit,
9440
+ // - double check by re-running the stage layout with the non overflow bounds:
9441
+ let forceRerunAsOverflow = false;
9442
+ if (roomLayoutHasOverlow && !stageLayout.hasOverflow) {
9443
+ const _stageLayout = calculateStageLayout(Object.assign(Object.assign({}, stableStageLayoutProps), { containerBounds: makeBounds({
9444
+ width: stageBounds.width,
9445
+ height: stageBounds.height - BOTTOM_TOOLBAR_HEIGHT, // override "stable" prop
9446
+ }), isPortrait: stageLayoutIsPortrait, hasConstrainedOverflow }));
9447
+ // If it turns out we can't fit, force re-layout as overflow:
9448
+ if (_stageLayout.hasOverflow) {
9449
+ forceRerunAsOverflow = true;
9450
+ }
9451
+ }
9452
+ // If subgrid cannot fit, re-run the stage layout in overflow:
9453
+ if (forceRerunAsOverflow || stageLayout.hasOverflow) {
9454
+ stageLayout = calculateStageLayout(Object.assign(Object.assign({}, stableStageLayoutProps), { isPortrait: true, hasConstrainedOverflow }));
9455
+ }
9456
+ const videosContainerLayout = calculateVideosContainerLayout({
9457
+ containerBounds: stageLayout.videosContainer.bounds,
9458
+ containerOrigin: stageLayout.videosContainer.origin,
9459
+ gridGap,
9460
+ supersizedContentAspectRatio,
9461
+ hasPresentationContent,
9462
+ hasPresentationGrid,
9463
+ hasVideoContent,
9464
+ minGridBounds,
9465
+ });
9466
+ const { gridLayout, presentationGridLayout } = calculateGridLayouts({
9467
+ gridGap: videoGridGap,
9468
+ isConstrained,
9469
+ presentationVideos,
9470
+ videos,
9471
+ videosContainerLayout,
9472
+ maxGridWidth,
9473
+ });
9474
+ const floatingLayout = calculateFloatingLayout({
9475
+ roomBounds,
9476
+ containerFrame: frame,
9477
+ floatingVideo,
9478
+ videoControlsHeight,
9479
+ });
9480
+ // Nudge containers closer to each other to get pleasing layouts with less extreme
9481
+ // negative space. It's opt in because debugging is a lot easier with this behavior off:
9482
+ if (rebalanceLayout) {
9483
+ rebalanceLayoutInPlace({
9484
+ videosContainerLayout,
9485
+ gridLayout,
9486
+ presentationGridLayout,
9487
+ gridGap,
9488
+ });
9489
+ }
9490
+ return {
9491
+ isPortrait: stageLayout.isPortrait,
9492
+ hasOverflow: stageLayout.hasOverflow,
9493
+ bounds: makeBounds({
9494
+ height: frame.bounds.height,
9495
+ width: frame.bounds.width,
9496
+ }),
9497
+ gridGap,
9498
+ presentationGrid: Object.assign(Object.assign({}, videosContainerLayout.presentationGrid), { cells: presentationGridLayout.videoCells, paddings: makeBox({
9499
+ top: presentationGridLayout.paddings.top + presentationGridLayout.extraVerticalPadding,
9500
+ bottom: presentationGridLayout.paddings.bottom + presentationGridLayout.extraVerticalPadding,
9501
+ left: presentationGridLayout.paddings.left + presentationGridLayout.extraHorizontalPadding,
9502
+ right: presentationGridLayout.paddings.right + presentationGridLayout.extraHorizontalPadding,
9503
+ }) }),
9504
+ videoGrid: Object.assign(Object.assign({}, videosContainerLayout.videoGrid), { cells: gridLayout.videoCells, paddings: makeBox({
9505
+ top: gridLayout.paddings.top + gridLayout.extraVerticalPadding,
9506
+ bottom: gridLayout.paddings.bottom + gridLayout.extraVerticalPadding,
9507
+ left: gridLayout.paddings.left + gridLayout.extraHorizontalPadding,
9508
+ right: gridLayout.paddings.right + gridLayout.extraHorizontalPadding,
9509
+ }) }),
9510
+ floatingContent: Object.assign(Object.assign({}, floatingLayout), floatingVideo),
9511
+ };
9512
+ }
9513
+
9514
+ function makeVideoCellView({ aspectRatio, avatarSize, cellPaddings, client = undefined, isDraggable = true, isPlaceholder = false, isSubgrid = false, }) {
9515
+ return {
9516
+ aspectRatio: aspectRatio || 16 / 9,
9517
+ avatarSize,
9518
+ cellPaddings,
9519
+ client,
9520
+ clientId: (client === null || client === void 0 ? void 0 : client.id) || "",
9521
+ isDraggable,
9522
+ isPlaceholder,
9523
+ isSubgrid,
9524
+ type: "video",
9525
+ };
9526
+ }
9527
+
9528
+ function GridVideoCellView({ cell, participant, render, onSetAspectRatio, onResize, }) {
9529
+ const handleAspectRatioChange = React.useCallback(({ ar }) => {
9530
+ if (ar !== cell.aspectRatio) {
9531
+ onSetAspectRatio({ aspectRatio: ar });
9532
+ }
9533
+ }, [cell.aspectRatio, onSetAspectRatio]);
9534
+ return (React.createElement("div", { style: {
9535
+ position: "absolute",
9536
+ width: cell.bounds.width,
9537
+ height: cell.bounds.height,
9538
+ boxSizing: "border-box",
9539
+ top: cell.origin.top,
9540
+ left: cell.origin.left,
9541
+ } }, render ? (render()) : participant.stream ? (React.createElement(VideoView, { stream: participant.stream, onSetAspectRatio: ({ aspectRatio }) => handleAspectRatioChange({ ar: aspectRatio }), onResize: onResize })) : null));
9542
+ }
9543
+ function Grid({ roomConnection, renderParticipant, videoGridGap = 0 }) {
9544
+ const { remoteParticipants, localParticipant } = roomConnection.state;
9545
+ const gridRef = React.useRef(null);
9546
+ const [containerFrame, setContainerFrame] = React.useState(null);
9547
+ const [aspectRatios, setAspectRatios] = React.useState([]);
9548
+ // Calculate container frame on resize
9549
+ React.useEffect(() => {
9550
+ if (!gridRef.current) {
9551
+ return;
9552
+ }
9553
+ const resizeObserver = new ResizeObserver(debounce(() => {
9554
+ var _a, _b;
9555
+ setContainerFrame(makeFrame({
9556
+ width: (_a = gridRef.current) === null || _a === void 0 ? void 0 : _a.clientWidth,
9557
+ height: (_b = gridRef.current) === null || _b === void 0 ? void 0 : _b.clientHeight,
9558
+ }));
9559
+ }, { delay: 60 }));
9560
+ resizeObserver.observe(gridRef.current);
9561
+ return () => {
9562
+ resizeObserver.disconnect();
9563
+ };
9564
+ }, []);
9565
+ // Merge local and remote participants
9566
+ const participants = React.useMemo(() => {
9567
+ return [...(localParticipant ? [localParticipant] : []), ...remoteParticipants];
9568
+ }, [remoteParticipants, localParticipant]);
9569
+ // Make video cells
9570
+ const videoCells = React.useMemo(() => {
9571
+ return participants.map((participant) => {
9572
+ var _a;
9573
+ const aspectRatio = (_a = aspectRatios.find((item) => item.clientId === (participant === null || participant === void 0 ? void 0 : participant.id))) === null || _a === void 0 ? void 0 : _a.aspectRatio;
9574
+ return makeVideoCellView({
9575
+ aspectRatio: aspectRatio !== null && aspectRatio !== void 0 ? aspectRatio : 16 / 9,
9576
+ avatarSize: 0,
9577
+ cellPaddings: 10,
9578
+ client: participant,
9579
+ });
9580
+ });
9581
+ }, [participants, aspectRatios]);
9582
+ // Calculate stage layout
9583
+ const stageLayout = React.useMemo(() => {
9584
+ if (!containerFrame)
9585
+ return null;
9586
+ return calculateLayout({
9587
+ frame: containerFrame,
9588
+ gridGap: 0,
9589
+ isConstrained: false,
9590
+ roomBounds: containerFrame.bounds,
9591
+ videos: videoCells,
9592
+ videoGridGap,
9593
+ });
9594
+ }, [containerFrame, videoCells, videoGridGap]);
9595
+ // Handle resize
9596
+ const handleResize = React.useCallback(({ width, height, stream }) => {
9597
+ if (!roomConnection._ref)
9598
+ return;
9599
+ roomConnection._ref.dispatch(doRtcReportStreamResolution({ streamId: stream.id, width, height }));
9600
+ }, [localParticipant, roomConnection._ref]);
9601
+ return (React.createElement("div", { ref: gridRef, style: {
9602
+ width: "100%",
9603
+ height: "100%",
9604
+ position: "relative",
9605
+ } }, participants.map((participant, i) => {
9606
+ const cell = stageLayout === null || stageLayout === void 0 ? void 0 : stageLayout.videoGrid.cells[i];
9607
+ if (!cell || !participant || !participant.stream || !cell.clientId)
9608
+ return null;
9609
+ return (React.createElement(GridVideoCellView, { key: cell.clientId, cell: cell, participant: participant, render: renderParticipant ? () => renderParticipant({ cell, participant }) : undefined, onResize: handleResize, onSetAspectRatio: ({ aspectRatio }) => {
9610
+ setAspectRatios((prev) => {
9611
+ const index = prev.findIndex((item) => item.clientId === cell.clientId);
9612
+ if (index === -1) {
9613
+ return [...prev, { clientId: cell.clientId, aspectRatio }];
9614
+ }
9615
+ return [
9616
+ ...prev.slice(0, index),
9617
+ { clientId: cell.clientId, aspectRatio },
9618
+ ...prev.slice(index + 1),
9619
+ ];
9620
+ });
9621
+ } }));
9622
+ })));
9623
+ }
9624
+
9625
+ export { Grid as VideoGrid, VideoView, sdkVersion, useLocalMedia, useRoomConnection };