bitmovin-player-ui
Advanced tools
Comparing version 2.10.1 to 2.10.2
@@ -7,2 +7,12 @@ # Change Log | ||
## [2.10.2] | ||
### Changed | ||
- Rewritten CEA-608 text layouting | ||
- Greatly simplified CEA-608 CSS style (`.{prefix}-ui-subtitle-overlay.{prefix}-cea608`) | ||
- Calculate CEA-608 font size only with active CEA-608 cues | ||
### Fixed | ||
- Overlapping CEA-608 texts with large player aspect ratios | ||
## [2.10.1] | ||
@@ -260,2 +270,3 @@ | ||
[2.10.2]: https://github.com/bitmovin/bitmovin-player-ui/compare/v2.10.1...v2.10.2 | ||
[2.10.1]: https://github.com/bitmovin/bitmovin-player-ui/compare/v2.10.0...v2.10.1 | ||
@@ -262,0 +273,0 @@ [2.10.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v2.9.0...v2.10.0 |
@@ -10,11 +10,8 @@ import { Container, ContainerConfig } from './container'; | ||
private previewSubtitle; | ||
private cea608lineHeight; | ||
private isCEA608; | ||
private static readonly CLASS_CONTROLBAR_VISIBLE; | ||
private static readonly CLASS_CEA_608; | ||
private static readonly CEA608_NUM_ROWS; | ||
private static readonly CEA608_NUM_COLUMNS; | ||
private static readonly CEA608_WIDTH; | ||
private static readonly CEA608_FONT_SPACING; | ||
private static readonly CEA608_LINE_COEF; | ||
private static readonly CEA608_LINE_TO_FONT_SIZE; | ||
private static readonly CEA608_ROW_OFFSET; | ||
private static readonly CEA608_COLUMN_OFFSET; | ||
constructor(config?: ContainerConfig); | ||
@@ -21,0 +18,0 @@ configure(player: bitmovin.PlayerAPI, uimanager: UIInstanceManager): void; |
@@ -16,3 +16,2 @@ "use strict"; | ||
var controlbar_1 = require("./controlbar"); | ||
var dom_1 = require("../dom"); | ||
/** | ||
@@ -26,3 +25,2 @@ * Overlays the player to display subtitles. | ||
var _this = _super.call(this, config) || this; | ||
_this.isCEA608 = false; | ||
_this.previewSubtitleActive = false; | ||
@@ -41,2 +39,8 @@ _this.previewSubtitle = new SubtitleLabel({ text: 'example subtitle' }); | ||
player.addEventHandler(player.EVENT.ON_CUE_ENTER, function (event) { | ||
// Sanitize cue data (must be done before the cue ID is generated in subtitleManager.cueEnter) | ||
if (event.position) { | ||
// Sometimes the positions are undefined, we assume them to be zero | ||
event.position.row = event.position.row || 0; | ||
event.position.column = event.position.column || 0; | ||
} | ||
var labelToAdd = subtitleManager.cueEnter(event); | ||
@@ -94,25 +98,26 @@ if (_this.previewSubtitleActive) { | ||
var _this = this; | ||
var ratio = 1; | ||
// The calculated font size | ||
var fontSize = 0; | ||
// The required letter spacing spread the text characters evenly across the grid | ||
var fontLetterSpacing = 0; | ||
// Flag telling if a font size calculation is required of if the current values are valid | ||
var fontSizeCalculationRequired = true; | ||
// Flag telling if the CEA-608 mode is enabled | ||
var enabled = false; | ||
var updateCEA608FontSize = function () { | ||
var dummyText = 'aaaaaaaaaa'; | ||
var label = new SubtitleLabel({ | ||
// One letter label used to calculate the height width ratio of the font | ||
// Works because we are using a monospace font for cea 608 | ||
// Using a longer string increases precision due to width being an integer | ||
text: dummyText, | ||
var dummyLabel = new SubtitleLabel({ text: 'X' }); | ||
dummyLabel.getDomElement().css({ | ||
// By using a large font size we do not need to use multiple letters and can get still an | ||
// accurate measurement even though the returned size is an integer value | ||
'font-size': '200px', | ||
'line-height': '200px', | ||
'visibility': 'hidden', | ||
}); | ||
var domElement = label.getDomElement(); | ||
domElement.css({ | ||
'color': 'rgba(0, 0, 0, 0)', | ||
'font-size': '30px', | ||
'line-height': '30px', | ||
'top': '0', | ||
'left': '0', | ||
}); | ||
_this.addComponent(label); | ||
_this.addComponent(dummyLabel); | ||
_this.updateComponents(); | ||
_this.show(); | ||
var width = domElement.width() / dummyText.length; | ||
var height = domElement.height(); | ||
_this.removeComponent(label); | ||
var dummyLabelCharWidth = dummyLabel.getDomElement().width(); | ||
var dummyLabelCharHeight = dummyLabel.getDomElement().height(); | ||
var fontSizeRatio = dummyLabelCharWidth / dummyLabelCharHeight; | ||
_this.removeComponent(dummyLabel); | ||
_this.updateComponents(); | ||
@@ -122,18 +127,42 @@ if (!_this.subtitleManager.hasCues) { | ||
} | ||
ratio = height / width; | ||
_this.cea608lineHeight = (new dom_1.DOM(player.getFigure()).width()) * (SubtitleOverlay.CEA608_WIDTH / SubtitleOverlay.CEA608_NUM_COLUMNS) * ratio * SubtitleOverlay.CEA608_FONT_SPACING; | ||
if (_this.isCEA608) { | ||
for (var _i = 0, _a = _this.getComponents(); _i < _a.length; _i++) { | ||
var label_2 = _a[_i]; | ||
if (label_2 instanceof SubtitleLabel) { | ||
// Only element with left property are cea-608 | ||
var domElement_1 = label_2.getDomElement(); | ||
domElement_1.css({ | ||
'font-size': SubtitleOverlay.CEA608_LINE_TO_FONT_SIZE * _this.cea608lineHeight + "px", | ||
}); | ||
} | ||
// The size ratio of the letter grid | ||
var fontGridSizeRatio = (dummyLabelCharWidth * SubtitleOverlay.CEA608_NUM_COLUMNS) / | ||
(dummyLabelCharHeight * SubtitleOverlay.CEA608_NUM_ROWS); | ||
// The size ratio of the available space for the grid | ||
var subtitleOverlaySizeRatio = _this.getDomElement().width() / _this.getDomElement().height(); | ||
if (subtitleOverlaySizeRatio > fontGridSizeRatio) { | ||
// When the available space is wider than the text grid, the font size is simply | ||
// determined by the height of the available space. | ||
fontSize = _this.getDomElement().height() / SubtitleOverlay.CEA608_NUM_ROWS; | ||
// Calculate the additional letter spacing required to evenly spread the text across the grid's width | ||
var gridSlotWidth = _this.getDomElement().width() / SubtitleOverlay.CEA608_NUM_COLUMNS; | ||
var fontCharWidth = fontSize * fontSizeRatio; | ||
fontLetterSpacing = gridSlotWidth - fontCharWidth; | ||
} | ||
else { | ||
// When the available space is not wide enough, texts would vertically overlap if we take | ||
// the height as a base for the font size, so we need to limit the height. We do that | ||
// by determining the font size by the width of the available space. | ||
fontSize = _this.getDomElement().width() / SubtitleOverlay.CEA608_NUM_COLUMNS / fontSizeRatio; | ||
fontLetterSpacing = 0; | ||
} | ||
// Update font-size of all active subtitle labels | ||
for (var _i = 0, _a = _this.getComponents(); _i < _a.length; _i++) { | ||
var label = _a[_i]; | ||
if (label instanceof SubtitleLabel) { | ||
label.getDomElement().css({ | ||
'font-size': fontSize + "px", | ||
'letter-spacing': fontLetterSpacing + "px", | ||
}); | ||
} | ||
} | ||
}; | ||
player.addEventHandler(player.EVENT.ON_PLAYER_RESIZE, updateCEA608FontSize); | ||
player.addEventHandler(player.EVENT.ON_PLAYER_RESIZE, function () { | ||
if (enabled) { | ||
updateCEA608FontSize(); | ||
} | ||
else { | ||
fontSizeCalculationRequired = true; | ||
} | ||
}); | ||
player.addEventHandler(player.EVENT.ON_CUE_ENTER, function (event) { | ||
@@ -146,6 +175,13 @@ var isCEA608 = event.position != null; | ||
var labels = _this.subtitleManager.getCues(event); | ||
if (!_this.isCEA608) { | ||
_this.isCEA608 = true; | ||
if (!enabled) { | ||
enabled = true; | ||
_this.getDomElement().addClass(_this.prefixCss(SubtitleOverlay.CLASS_CEA_608)); | ||
updateCEA608FontSize(); | ||
// We conditionally update the font size by this flag here to avoid updating every time a subtitle | ||
// is added into an empty overlay. Because we reset the overlay when all subtitles are gone, this | ||
// would trigger an unnecessary update every time, but it's only required under certain conditions, | ||
// e.g. after the player size has changed. | ||
if (fontSizeCalculationRequired) { | ||
updateCEA608FontSize(); | ||
fontSizeCalculationRequired = false; | ||
} | ||
} | ||
@@ -155,5 +191,6 @@ for (var _i = 0, labels_1 = labels; _i < labels_1.length; _i++) { | ||
label.getDomElement().css({ | ||
'left': event.position.column / ratio + "em", | ||
'top': event.position.row * SubtitleOverlay.CEA608_LINE_COEF + "%", | ||
'font-size': SubtitleOverlay.CEA608_LINE_TO_FONT_SIZE * _this.cea608lineHeight + "px", | ||
'left': event.position.column * SubtitleOverlay.CEA608_COLUMN_OFFSET + "%", | ||
'top': event.position.row * SubtitleOverlay.CEA608_ROW_OFFSET + "%", | ||
'font-size': fontSize + "px", | ||
'letter-spacing': fontLetterSpacing + "px", | ||
}); | ||
@@ -164,4 +201,11 @@ } | ||
_this.getDomElement().removeClass(_this.prefixCss(SubtitleOverlay.CLASS_CEA_608)); | ||
_this.isCEA608 = false; | ||
enabled = false; | ||
}; | ||
player.addEventHandler(player.EVENT.ON_CUE_EXIT, function () { | ||
if (!_this.subtitleManager.hasCues) { | ||
// Disable CEA-608 mode when all subtitles are gone (to allow correct formatting and | ||
// display of other types of subtitles, e.g. the formatting preview subtitle) | ||
reset(); | ||
} | ||
}); | ||
player.addEventHandler(player.EVENT.ON_SOURCE_UNLOADED, reset); | ||
@@ -185,13 +229,10 @@ player.addEventHandler(player.EVENT.ON_SUBTITLE_CHANGED, reset); | ||
SubtitleOverlay.CLASS_CEA_608 = 'cea608'; | ||
// The number of columns in a cea608 line | ||
// The number of rows in a cea608 grid | ||
SubtitleOverlay.CEA608_NUM_ROWS = 15; | ||
// The number of columns in a cea608 grid | ||
SubtitleOverlay.CEA608_NUM_COLUMNS = 32; | ||
// 80% is the width of the space where CEA608 captions are displayed | ||
SubtitleOverlay.CEA608_WIDTH = 0.8; | ||
// The actual font width is 1 + 0.11 letter-spacing | ||
SubtitleOverlay.CEA608_FONT_SPACING = 1.11; | ||
// 6.66 = 100/15 the number of possible lines | ||
SubtitleOverlay.CEA608_LINE_COEF = 6.66; | ||
// To avoid having the font too big and overlap, while still have the proper width, | ||
// the font must be sized down, the width is then compensated by letter-spacing | ||
SubtitleOverlay.CEA608_LINE_TO_FONT_SIZE = 0.9; | ||
// The offset in percent for one row (which is also the height of a row) | ||
SubtitleOverlay.CEA608_ROW_OFFSET = 100 / SubtitleOverlay.CEA608_NUM_ROWS; | ||
// The offset in percent for one column (which is also the width of a column) | ||
SubtitleOverlay.CEA608_COLUMN_OFFSET = 100 / SubtitleOverlay.CEA608_NUM_COLUMNS; | ||
return SubtitleOverlay; | ||
@@ -221,3 +262,3 @@ }(container_1.Container)); | ||
* The start time plus the text should make a unique identifier, and in the only case where a collision | ||
* can happen, two similar texts will be displayed at a similar time. | ||
* can happen, two similar texts will be displayed at a similar time and a similar position (or without position). | ||
* The start time should always be known, because it is required to schedule the ON_CUE_ENTER event. The end time | ||
@@ -229,3 +270,7 @@ * must not necessarily be known and therefore cannot be used for the ID. | ||
ActiveSubtitleManager.calculateId = function (event) { | ||
return event.start + event.text; | ||
var id = event.start + '-' + event.text; | ||
if (event.position) { | ||
id += '-' + event.position.row + '-' + event.position.column; | ||
} | ||
return id; | ||
}; | ||
@@ -232,0 +277,0 @@ /** |
@@ -96,3 +96,3 @@ "use strict"; | ||
var playerui = { | ||
version: '2.10.1', | ||
version: '2.10.2', | ||
// Management | ||
@@ -99,0 +99,0 @@ UIManager: uimanager_1.UIManager, |
{ | ||
"name": "bitmovin-player-ui", | ||
"version": "2.10.1", | ||
"version": "2.10.2", | ||
"description": "Bitmovin Player UI Framework", | ||
@@ -5,0 +5,0 @@ "main": "dist/js/framework/main.js", |
@@ -7,3 +7,2 @@ import {Container, ContainerConfig} from './container'; | ||
import {ControlBar} from './controlbar'; | ||
import {DOM} from '../dom'; | ||
@@ -18,18 +17,13 @@ /** | ||
private previewSubtitle: SubtitleLabel; | ||
private cea608lineHeight: number; | ||
private isCEA608: boolean = false; | ||
private static readonly CLASS_CONTROLBAR_VISIBLE = 'controlbar-visible'; | ||
private static readonly CLASS_CEA_608 = 'cea608'; | ||
// The number of columns in a cea608 line | ||
// The number of rows in a cea608 grid | ||
private static readonly CEA608_NUM_ROWS = 15; | ||
// The number of columns in a cea608 grid | ||
private static readonly CEA608_NUM_COLUMNS = 32; | ||
// 80% is the width of the space where CEA608 captions are displayed | ||
private static readonly CEA608_WIDTH = 0.8; | ||
// The actual font width is 1 + 0.11 letter-spacing | ||
private static readonly CEA608_FONT_SPACING = 1.11; | ||
// 6.66 = 100/15 the number of possible lines | ||
private static readonly CEA608_LINE_COEF = 6.66; | ||
// To avoid having the font too big and overlap, while still have the proper width, | ||
// the font must be sized down, the width is then compensated by letter-spacing | ||
private static readonly CEA608_LINE_TO_FONT_SIZE = 0.9; | ||
// The offset in percent for one row (which is also the height of a row) | ||
private static readonly CEA608_ROW_OFFSET = 100 / SubtitleOverlay.CEA608_NUM_ROWS; | ||
// The offset in percent for one column (which is also the width of a column) | ||
private static readonly CEA608_COLUMN_OFFSET = 100 / SubtitleOverlay.CEA608_NUM_COLUMNS; | ||
@@ -54,2 +48,9 @@ constructor(config: ContainerConfig = {}) { | ||
player.addEventHandler(player.EVENT.ON_CUE_ENTER, (event: SubtitleCueEvent) => { | ||
// Sanitize cue data (must be done before the cue ID is generated in subtitleManager.cueEnter) | ||
if (event.position) { | ||
// Sometimes the positions are undefined, we assume them to be zero | ||
event.position.row = event.position.row || 0; | ||
event.position.column = event.position.column || 0; | ||
} | ||
let labelToAdd = subtitleManager.cueEnter(event); | ||
@@ -114,27 +115,29 @@ | ||
configureCea608Captions(player: bitmovin.PlayerAPI, uimanager: UIInstanceManager): void { | ||
let ratio = 1; | ||
let updateCEA608FontSize = () => { | ||
let dummyText = 'aaaaaaaaaa'; | ||
let label = new SubtitleLabel({ | ||
// One letter label used to calculate the height width ratio of the font | ||
// Works because we are using a monospace font for cea 608 | ||
// Using a longer string increases precision due to width being an integer | ||
text: dummyText, | ||
// The calculated font size | ||
let fontSize = 0; | ||
// The required letter spacing spread the text characters evenly across the grid | ||
let fontLetterSpacing = 0; | ||
// Flag telling if a font size calculation is required of if the current values are valid | ||
let fontSizeCalculationRequired = true; | ||
// Flag telling if the CEA-608 mode is enabled | ||
let enabled = false; | ||
const updateCEA608FontSize = () => { | ||
const dummyLabel = new SubtitleLabel({ text: 'X' }); | ||
dummyLabel.getDomElement().css({ | ||
// By using a large font size we do not need to use multiple letters and can get still an | ||
// accurate measurement even though the returned size is an integer value | ||
'font-size': '200px', | ||
'line-height': '200px', | ||
'visibility': 'hidden', | ||
}); | ||
let domElement = label.getDomElement(); | ||
domElement.css({ | ||
'color': 'rgba(0, 0, 0, 0)', | ||
'font-size': '30px', | ||
'line-height': '30px', | ||
'top': '0', | ||
'left': '0', | ||
}); | ||
this.addComponent(label); | ||
this.addComponent(dummyLabel); | ||
this.updateComponents(); | ||
this.show(); | ||
let width = domElement.width() / dummyText.length; | ||
let height = domElement.height(); | ||
const dummyLabelCharWidth = dummyLabel.getDomElement().width(); | ||
const dummyLabelCharHeight = dummyLabel.getDomElement().height(); | ||
const fontSizeRatio = dummyLabelCharWidth / dummyLabelCharHeight; | ||
this.removeComponent(label); | ||
this.removeComponent(dummyLabel); | ||
this.updateComponents(); | ||
@@ -145,21 +148,46 @@ if (!this.subtitleManager.hasCues) { | ||
ratio = height / width; | ||
this.cea608lineHeight = (new DOM(player.getFigure()).width()) * (SubtitleOverlay.CEA608_WIDTH / SubtitleOverlay.CEA608_NUM_COLUMNS) * ratio * SubtitleOverlay.CEA608_FONT_SPACING; | ||
// The size ratio of the letter grid | ||
const fontGridSizeRatio = (dummyLabelCharWidth * SubtitleOverlay.CEA608_NUM_COLUMNS) / | ||
(dummyLabelCharHeight * SubtitleOverlay.CEA608_NUM_ROWS); | ||
// The size ratio of the available space for the grid | ||
const subtitleOverlaySizeRatio = this.getDomElement().width() / this.getDomElement().height(); | ||
if (this.isCEA608) { | ||
for (let label of this.getComponents()) { | ||
if (label instanceof SubtitleLabel) { | ||
// Only element with left property are cea-608 | ||
let domElement = label.getDomElement(); | ||
domElement.css({ | ||
'font-size': `${SubtitleOverlay.CEA608_LINE_TO_FONT_SIZE * this.cea608lineHeight}px`, | ||
}); | ||
} | ||
if (subtitleOverlaySizeRatio > fontGridSizeRatio) { | ||
// When the available space is wider than the text grid, the font size is simply | ||
// determined by the height of the available space. | ||
fontSize = this.getDomElement().height() / SubtitleOverlay.CEA608_NUM_ROWS; | ||
// Calculate the additional letter spacing required to evenly spread the text across the grid's width | ||
const gridSlotWidth = this.getDomElement().width() / SubtitleOverlay.CEA608_NUM_COLUMNS; | ||
const fontCharWidth = fontSize * fontSizeRatio; | ||
fontLetterSpacing = gridSlotWidth - fontCharWidth; | ||
} else { | ||
// When the available space is not wide enough, texts would vertically overlap if we take | ||
// the height as a base for the font size, so we need to limit the height. We do that | ||
// by determining the font size by the width of the available space. | ||
fontSize = this.getDomElement().width() / SubtitleOverlay.CEA608_NUM_COLUMNS / fontSizeRatio; | ||
fontLetterSpacing = 0; | ||
} | ||
// Update font-size of all active subtitle labels | ||
for (let label of this.getComponents()) { | ||
if (label instanceof SubtitleLabel) { | ||
label.getDomElement().css({ | ||
'font-size': `${fontSize}px`, | ||
'letter-spacing': `${fontLetterSpacing}px`, | ||
}); | ||
} | ||
} | ||
}; | ||
player.addEventHandler(player.EVENT.ON_PLAYER_RESIZE, updateCEA608FontSize); | ||
player.addEventHandler(player.EVENT.ON_PLAYER_RESIZE, () => { | ||
if (enabled) { | ||
updateCEA608FontSize(); | ||
} else { | ||
fontSizeCalculationRequired = true; | ||
} | ||
}); | ||
player.addEventHandler(player.EVENT.ON_CUE_ENTER, (event: SubtitleCueEvent) => { | ||
let isCEA608 = event.position != null; | ||
const isCEA608 = event.position != null; | ||
if (!isCEA608) { | ||
@@ -170,14 +198,23 @@ // Skip all non-CEA608 cues | ||
let labels = this.subtitleManager.getCues(event); | ||
const labels = this.subtitleManager.getCues(event); | ||
if (!this.isCEA608) { | ||
this.isCEA608 = true; | ||
if (!enabled) { | ||
enabled = true; | ||
this.getDomElement().addClass(this.prefixCss(SubtitleOverlay.CLASS_CEA_608)); | ||
updateCEA608FontSize(); | ||
// We conditionally update the font size by this flag here to avoid updating every time a subtitle | ||
// is added into an empty overlay. Because we reset the overlay when all subtitles are gone, this | ||
// would trigger an unnecessary update every time, but it's only required under certain conditions, | ||
// e.g. after the player size has changed. | ||
if (fontSizeCalculationRequired) { | ||
updateCEA608FontSize(); | ||
fontSizeCalculationRequired = false; | ||
} | ||
} | ||
for (let label of labels) { | ||
label.getDomElement().css({ | ||
'left': `${event.position.column / ratio}em`, | ||
'top': `${event.position.row * SubtitleOverlay.CEA608_LINE_COEF}%`, | ||
'font-size': `${SubtitleOverlay.CEA608_LINE_TO_FONT_SIZE * this.cea608lineHeight}px`, | ||
'left': `${event.position.column * SubtitleOverlay.CEA608_COLUMN_OFFSET}%`, | ||
'top': `${event.position.row * SubtitleOverlay.CEA608_ROW_OFFSET}%`, | ||
'font-size': `${fontSize}px`, | ||
'letter-spacing': `${fontLetterSpacing}px`, | ||
}); | ||
@@ -187,6 +224,15 @@ } | ||
let reset = () => { | ||
const reset = () => { | ||
this.getDomElement().removeClass(this.prefixCss(SubtitleOverlay.CLASS_CEA_608)); | ||
this.isCEA608 = false; | ||
enabled = false; | ||
}; | ||
player.addEventHandler(player.EVENT.ON_CUE_EXIT, () => { | ||
if (!this.subtitleManager.hasCues) { | ||
// Disable CEA-608 mode when all subtitles are gone (to allow correct formatting and | ||
// display of other types of subtitles, e.g. the formatting preview subtitle) | ||
reset(); | ||
} | ||
}); | ||
player.addEventHandler(player.EVENT.ON_SOURCE_UNLOADED, reset); | ||
@@ -246,3 +292,3 @@ player.addEventHandler(player.EVENT.ON_SUBTITLE_CHANGED, reset); | ||
* The start time plus the text should make a unique identifier, and in the only case where a collision | ||
* can happen, two similar texts will be displayed at a similar time. | ||
* can happen, two similar texts will be displayed at a similar time and a similar position (or without position). | ||
* The start time should always be known, because it is required to schedule the ON_CUE_ENTER event. The end time | ||
@@ -254,3 +300,9 @@ * must not necessarily be known and therefore cannot be used for the ID. | ||
private static calculateId(event: SubtitleCueEvent): string { | ||
return event.start + event.text; | ||
let id = event.start + '-' + event.text; | ||
if (event.position) { | ||
id += '-' + event.position.row + '-' + event.position.column; | ||
} | ||
return id; | ||
} | ||
@@ -257,0 +309,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
3579488
37287