From 1cd47271ba802faef8f3c7cfd40c6889d7c25b29 Mon Sep 17 00:00:00 2001 From: Tsachi Shlidor Date: Tue, 1 Apr 2025 16:27:35 +0300 Subject: [PATCH 01/12] feat: visual search plugin --- docs/es-modules/index.html | 1 + docs/es-modules/visual-search.html | 62 ++++++++ docs/index.html | 1 + docs/visual-search.html | 121 +++++++++++++++ src/assets/icon-font/README.md | 4 + src/assets/icon-font/VideoJS.svg | 114 ++++++-------- src/assets/icon-font/VideoJS.ttf | Bin 9200 -> 7220 bytes src/assets/icon-font/VideoJS.woff | Bin 5640 -> 4264 bytes src/assets/icon-font/icons.json | 21 +-- src/assets/styles/_icons.scss | 14 +- src/config/defaults.js | 1 + .../models/video-source/video-source.js | 18 ++- src/plugins/index.js | 4 +- .../visual-search/components/SearchButton.js | 18 +++ .../visual-search/components/SearchInput.js | 49 ++++++ .../visual-search/components/SearchResults.js | 45 ++++++ src/plugins/visual-search/index.js | 9 ++ src/plugins/visual-search/utils/mockData.js | 54 +++++++ src/plugins/visual-search/visual-search.js | 119 ++++++++++++++ src/plugins/visual-search/visual-search.scss | 145 ++++++++++++++++++ src/utils/get-analytics-player-options.js | 1 + src/validators/validators.js | 1 + src/video-player.const.js | 3 +- src/video-player.js | 14 ++ 24 files changed, 724 insertions(+), 95 deletions(-) create mode 100644 docs/es-modules/visual-search.html create mode 100644 docs/visual-search.html create mode 100644 src/plugins/visual-search/components/SearchButton.js create mode 100644 src/plugins/visual-search/components/SearchInput.js create mode 100644 src/plugins/visual-search/components/SearchResults.js create mode 100644 src/plugins/visual-search/index.js create mode 100644 src/plugins/visual-search/utils/mockData.js create mode 100644 src/plugins/visual-search/visual-search.js create mode 100644 src/plugins/visual-search/visual-search.scss diff --git a/docs/es-modules/index.html b/docs/es-modules/index.html index eae76ca39..5bb92eb3a 100644 --- a/docs/es-modules/index.html +++ b/docs/es-modules/index.html @@ -73,6 +73,7 @@

Code examples:

  • Subtitles & Captions
  • Video Transformations
  • VAST & VPAID Support
  • +
  • Visual Search
  • VR/360 Videos

  • /all build
  • diff --git a/docs/es-modules/visual-search.html b/docs/es-modules/visual-search.html new file mode 100644 index 000000000..2a34e4490 --- /dev/null +++ b/docs/es-modules/visual-search.html @@ -0,0 +1,62 @@ + + + + + Cloudinary Video Player + + + + + +
    + +

    Cloudinary Video Player

    +

    Visual Search

    + + + +

    + Full documentation +

    +
    + + + + + + + diff --git a/docs/index.html b/docs/index.html index bd4e16c96..f7d3e190f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -79,6 +79,7 @@

    Some code examples:

  • Subtitles & Captions
  • Video Transformations
  • VAST & VPAID Support
  • +
  • Visual Search
  • VR/360 Videos

  • Embedded (iframe) player
  • diff --git a/docs/visual-search.html b/docs/visual-search.html new file mode 100644 index 000000000..e21b4858c --- /dev/null +++ b/docs/visual-search.html @@ -0,0 +1,121 @@ + + + + + + Cloudinary Video Player - Visual Search + + + + + + + + + + + + + + + + + + +
    + +

    Cloudinary Video Player

    +

    Visual Search Plugin

    + + + +

    Playlist with mixed content

    + + + +

    + Full documentation +

    + +

    Example Code:

    +
    +      
    +<video
    +  id="player"
    +  playsinline
    +  controls
    +  class="cld-video-player cld-video-player-skin-dark"
    +  width="500"
    +></video>
    +      
    +      
    +// Initialize player with visual search plugin
    +const player = cloudinary.videoPlayer('player', {
    +  cloudName: 'demo',
    +  publicId: 'elephants',
    +  visualSearch: true
    +});
    +
    +      
    +    
    +
    + + + + \ No newline at end of file diff --git a/src/assets/icon-font/README.md b/src/assets/icon-font/README.md index caae8d76f..7e67054a1 100644 --- a/src/assets/icon-font/README.md +++ b/src/assets/icon-font/README.md @@ -3,3 +3,7 @@ ## How to generate an updated icon-font Use the utility from https://github.com/videojs/font with the custom icons in the `cld` folder and the configuration in the `icons.json` provided here. + +MUI3 icons can be found [here](https://github.com/google/material-design-icons/blob/3.0.2/sprites/css-sprite/sprite-action-black.png). + +Copy the generated `videojs-icons.scss` file to the `styles` folder. diff --git a/src/assets/icon-font/VideoJS.svg b/src/assets/icon-font/VideoJS.svg index 613c6bacc..1b27e37df 100755 --- a/src/assets/icon-font/VideoJS.svg +++ b/src/assets/icon-font/VideoJS.svg @@ -2,158 +2,146 @@ - + + horiz-adv-x="512" d=" M149.3333333333333 106.6666666666667V426.6666666666667L426.6666666666667 256L149.3333333333333 106.6666666666667z" /> + horiz-adv-x="512" d=" M213.3333333333333 160L341.3333333333333 256L213.3333333333333 352V160zM256 469.3333333333333C138.1333333333333 469.3333333333333 42.6666666666667 373.8666666666667 42.6666666666667 256S138.1333333333333 42.6666666666667 256 42.6666666666667S469.3333333333333 138.1333333333334 469.3333333333333 256S373.8666666666666 469.3333333333333 256 469.3333333333333zM256 85.3333333333334C161.92 85.3333333333334 85.3333333333333 161.92 85.3333333333333 256S161.92 426.6666666666667 256 426.6666666666667S426.6666666666667 350.0800000000001 426.6666666666667 256S350.08 85.3333333333334 256 85.3333333333334z" /> + horiz-adv-x="512" d=" M128 405.3333333333334H213.3333333333333V106.6666666666667H128V405.3333333333334zM298.6666666666667 405.3333333333334H384V106.6666666666667H298.6666666666667V405.3333333333334z" /> + horiz-adv-x="512" d=" M416 394.6666666666667C384 435.2 341.3333333333333 458.6666666666667 290.1333333333334 469.3333333333333V422.4C328.5333333333333 411.7333333333334 358.4 392.5333333333334 381.8666666666666 360.5333333333334C405.3333333333333 330.6666666666667 418.1333333333334 294.4000000000001 418.1333333333334 256C418.1333333333334 243.2000000000001 416 230.4000000000001 413.8666666666666 217.6C411.7333333333334 204.8 407.4666666666667 194.1333333333333 401.0666666666667 183.4666666666667L435.2 149.3333333333334C443.7333333333334 164.2666666666667 452.2666666666667 181.3333333333334 456.5333333333333 200.5333333333334C460.8 217.6 465.0666666666667 236.8 465.0666666666667 256C465.0666666666667 309.3333333333334 450.1333333333334 354.1333333333334 416 394.6666666666667zM200.53376 388.2666666666667L241.0666666666667 426.6666666666667V347.7333333333334L200.53376 388.2666666666667zM343.4666666666667 256V251.7333333333334C341.3333333333333 249.6 341.3333333333333 247.4666666666667 341.3333333333333 247.4666666666667L292.2666666666667 296.5333333333334V341.3333333333334C307.2 332.8 320 320 328.5333333333333 305.0666666666667C339.2 290.1333333333334 343.4666666666667 273.0666666666667 343.4666666666667 256zM121.6004266666667 319.9978666666667L19.200512 424.5312L51.2004266666667 456.5312L456.5333333333333 51.1978666666666L424.5333333333333 19.1978666666666L369.0666666666667 74.6645333333333C358.4 66.1312 345.6 59.7312000000001 332.8 55.4645333333334C320 49.0645333333334 307.2 44.7978666666667 292.2666666666667 42.6645333333333V91.7312000000001C296.9066666666667 91.7312000000001 302.6688 93.9754666666667 307.9274666666667 96.0213333333334C309.92 96.7978666666667 311.84 97.5445333333333 313.6 98.1312C316.6229333333333 99.6437333333333 319.3792 100.8874666666667 321.9605333333334 102.0522666666667C326.6688 104.1770666666666 330.8010666666667 106.0437333333334 334.9333333333333 108.7978666666667L241.0666666666667 202.6645333333334V85.3312L134.4004266666667 191.9978666666667H46.93376V319.9978666666667H121.6004266666667z" /> + horiz-adv-x="512" d=" M149.3333333333333 320V192H234.6666666666667L341.3333333333333 85.3333333333334V426.6666666666667L234.6666666666667 320H149.3333333333333z" /> + horiz-adv-x="512" d=" M102.3997866666667 192V320H189.8664533333334L296.5333333333333 426.6666666666667V85.3333333333334L189.8664533333334 192H102.3997866666667zM347.7333333333334 339.1978666666667C364.8 330.6645333333334 377.6 319.9978666666667 386.1333333333334 305.0645333333333C394.6666666666667 290.1312000000001 398.9333333333333 270.9312 398.9333333333333 253.8645333333334C398.9333333333333 236.7978666666667 394.6666666666667 219.7312 386.1333333333334 204.7978666666667C377.6 189.8645333333334 364.8 179.1978666666667 347.7333333333334 170.6645333333334V339.1978666666667z" /> + horiz-adv-x="512" d=" M416 394.6666666666667C384 435.2 341.3333333333333 458.6666666666667 290.1333333333334 469.3333333333333V422.4C328.5333333333333 411.7333333333334 358.4 392.5333333333334 381.8666666666666 362.6666666666667C405.3333333333333 332.8 418.1333333333334 296.5333333333334 418.1333333333334 258.1333333333334C418.1333333333334 219.7333333333334 405.3333333333333 183.4666666666667 381.8666666666666 153.6C358.4 121.6 326.4 102.4 290.1333333333334 91.7333333333334V42.6666666666667C341.3333333333333 53.3333333333334 381.8666666666666 76.8000000000001 416 117.3333333333334C448 157.8666666666667 465.0666666666667 202.6666666666667 465.0666666666667 256C465.0666666666667 309.3333333333334 448 354.1333333333334 416 394.6666666666667zM328.5333333333333 206.9333333333333C337.0666666666667 221.8666666666667 343.4666666666667 238.9333333333334 343.4666666666667 256C343.4666666666667 273.0666666666667 339.2 290.1333333333334 328.5333333333333 305.0666666666667C320 320 307.2 332.8 290.1333333333334 341.3333333333334V170.6666666666667C307.2 179.2000000000001 320 192 328.5333333333333 206.9333333333333zM46.9335466666667 192V320H134.4002133333334L241.0666666666667 426.6666666666667V85.3333333333334L134.4002133333334 192H46.9335466666667z" /> + horiz-adv-x="512" d=" M64 320V405.3333333333334V448H192V405.3333333333334H106.6666666666667V320H64zM320 405.3333333333334V448H448V405.3333333333334V320H405.3333333333333V405.3333333333334H320zM106.6666666666667 192H64V106.6666666666667V64H192V106.6666666666667H106.6666666666667V192zM320 106.6666666666667H405.3333333333333V192H448V106.6666666666667V64H320V106.6666666666667z" /> + horiz-adv-x="512" d=" M64 362.6666666666667H149.3333333333333V448H192V362.6666666666667V320H64V362.6666666666667zM362.6666666666667 448V362.6666666666667H448V320H320V362.6666666666667V448H362.6666666666667zM64 149.3333333333334H149.3333333333333V64H192V149.3333333333334V192H64V149.3333333333334zM320 64V149.3333333333334V192H448V149.3333333333334H362.6666666666667V64H320z" /> + horiz-adv-x="512" d=" M200.5333333333333 288L302.1866666666666 464.1066666666667C287.36 467.4133333333334 271.8933333333333 469.3333333333333 256 469.3333333333333C204.8 469.3333333333333 157.9733333333333 451.3066666666667 121.1733333333333 421.2266666666667L199.36 285.8666666666667L200.5333333333333 288zM459.52 320C439.8933333333333 382.4 392.32 432.2133333333334 331.6266666666666 455.2533333333333L253.5466666666667 320H459.52zM465.0666666666667 298.6666666666667H305.28L311.4666666666667 288L413.12 112C447.8933333333333 149.9733333333334 469.3333333333333 200.4266666666667 469.3333333333333 256C469.3333333333333 270.6133333333334 467.84 284.9066666666667 465.0666666666667 298.6666666666667zM182.08 256L98.88 400C64.1066666666667 362.0266666666667 42.6666666666667 311.5733333333334 42.6666666666667 256C42.6666666666667 241.3866666666667 44.16 227.0933333333334 46.9333333333333 213.3333333333334H206.72L182.0800000000001 256zM52.48 192C72.1066666666667 129.6 119.68 79.7866666666668 180.3733333333333 56.7466666666667L258.4533333333333 192H52.48zM292.9066666666667 192L209.7066666666667 47.8933333333334C224.64 44.5866666666667 240.1066666666667 42.6666666666667 256 42.6666666666667C307.2 42.6666666666667 354.0266666666667 60.6933333333333 390.8266666666667 90.7733333333333L312.64 226.1333333333334L292.9066666666667 192z" /> + horiz-adv-x="512" d=" M64 106.6666666666667V405.3333333333334H448V106.6666666666667H64zM140.30784 182.9738666666667H307.6928V204.3072H140.30784V182.9738666666667zM350.3594666666667 182.9738666666667H371.6928000000001V204.3072H350.3594666666667V182.9738666666667zM140.30784 259.2810666666667H161.64096V280.6144000000001H140.30784V259.2810666666667zM204.3076266666666 259.2810666666667H371.6928000000001V280.6144000000001H204.3076266666666V259.2810666666667z" /> + horiz-adv-x="512" d=" M405.3333333333333 426.6666666666667H106.6666666666667C83.0933333333333 426.6666666666667 64 407.5733333333334 64 384V128C64 104.4266666666667 83.0933333333333 85.3333333333334 106.6666666666667 85.3333333333334H405.3333333333333C428.9066666666667 85.3333333333334 448 104.4266666666667 448 128V384C448 407.5733333333334 428.9066666666667 426.6666666666667 405.3333333333333 426.6666666666667zM234.6666666666667 277.3333333333334H202.6666666666667V288H160V224H202.6666666666667V234.6666666666667H234.6666666666667V213.3333333333334C234.6666666666667 201.6 225.1733333333333 192 213.3333333333333 192H149.3333333333333C137.4933333333334 192 128 201.6 128 213.3333333333334V298.6666666666667C128 310.4000000000001 137.4933333333334 320 149.3333333333333 320H213.3333333333333C225.1733333333333 320 234.6666666666667 310.4000000000001 234.6666666666667 298.6666666666667V277.3333333333334zM384 277.3333333333334H352V288H309.3333333333333V224H352V234.6666666666667H384V213.3333333333334C384 201.6 374.5066666666667 192 362.6666666666667 192H298.6666666666667C286.8266666666667 192 277.3333333333333 201.6 277.3333333333333 213.3333333333334V298.6666666666667C277.3333333333333 310.4000000000001 286.8266666666667 320 298.6666666666667 320H362.6666666666667C374.5066666666667 320 384 310.4000000000001 384 298.6666666666667V277.3333333333334z" /> + horiz-adv-x="512" d=" M405.3333333333333 448H106.6666666666667C82.9866666666667 448 64 428.8 64 405.3333333333334V106.6666666666667C64 83.2 82.9866666666667 64 106.6666666666667 64H405.3333333333333C428.8 64 448 83.2 448 106.6666666666667V405.3333333333334C448 428.8 428.8 448 405.3333333333333 448zM234.6666666666667 192H202.6666666666667V234.6666666666667H160V192H128V320H160V266.6666666666667H202.6666666666667V320H234.6666666666667V192zM277.3333333333333 320H362.6666666666667C374.4 320 384 310.4000000000001 384 298.6666666666667V213.3333333333334C384 201.6 374.4 192 362.6666666666667 192H277.3333333333333V320zM309.3333333333333 224H352V288H309.3333333333333V224z" /> + horiz-adv-x="512" d=" M64 234.6666666666667H106.6666666666667V277.3333333333334H64V234.6666666666667zM64 149.3333333333334H106.6666666666667V192H64V149.3333333333334zM64 320H106.6666666666667V362.6666666666667H64V320zM149.3333333333333 234.6666666666667H448V277.3333333333334H149.3333333333333V234.6666666666667zM149.3333333333333 149.3333333333334H448V192H149.3333333333333V149.3333333333334zM149.3333333333333 362.6666666666667V320H448V362.6666666666667H149.3333333333333z" /> + horiz-adv-x="512" d=" M233.0624 40Q192 43.7376 156.5312 62.1312Q121.0624 80.5376 94.9376 109.8624Q68.8 139.2 54.1312 176.7936Q39.4624 214.3999999999999 39.4624 256.5376Q39.4624 340.8 94.9376 402.6624Q150.4 464.5376 233.6 473.0624V432.5376Q167.4624 422.9376 123.7376 373.0688Q80 323.2 80 256.5376Q80 189.8624 123.4688 140.0064Q166.9376 90.1376 233.0624 80.5376zM256 148.2624L147.7376 256L174.4 282.6624L237.3376000000001 219.7376V363.7376H274.6624000000001V219.7376L337.0624000000001 282.6624L363.7376000000001 256zM278.9376000000001 40V80.5376Q302.4000000000001 83.7376 324.2624000000001 92.8Q346.1376 101.8624 364.8 116.8L394.6624 87.4624Q369.6 67.2 340.2624 54.9376Q310.9376 42.6623999999999 278.9376 40zM365.8624000000001 395.7376000000001Q346.1376 410.1376 324.2624000000001 419.4688Q302.4000000000001 428.8 278.9376000000001 432.5376V473.0624Q310.9376000000001 469.8624 339.9936 457.6Q369.0624000000001 445.3376 394.6624000000001 425.6zM424.5376000000001 119.4624L395.7376000000001 148.2624Q410.6624 167.4624 419.4688000000001 189.3376Q428.2624 211.2 431.4624 234.6624H472.5376000000001Q468.8 202.6624 456.8064000000001 173.3376Q444.8 144 424.5376000000001 119.4624zM431.4624 278.4Q427.7376000000001 301.8624 419.2000000000001 323.9936Q410.6624 346.1376 395.7376000000001 364.8L426.1376 393.0624000000001Q445.8624 368.5376 457.6 339.2Q469.3376000000001 309.8624 472.5376000000001 278.4z" /> + horiz-adv-x="512" d=" M115.2 79.4666666666667Q100.8 79.4666666666667 89.8666666666667 90.1333333333334T78.9333333333333 115.7333333333334V197.8666666666667H115.2V115.7333333333334H396.2666666666667V197.8666666666667H432.5333333333334V115.7333333333334Q432.5333333333334 100.8000000000001 421.8666666666667 90.1333333333334T396.2666666666667 79.4666666666667zM256 169.6L148.2666666666667 276.8L174.4 302.9333333333334L237.8666666666667 239.4666666666667V435.7333333333334H274.1333333333334V239.4666666666667L337.6 302.9333333333334L363.7333333333334 276.8000000000001z" /> + horiz-adv-x="512" d=" M104.5333333333333 80V116.8000000000001H407.4666666666667V80zM202.6666666666667 176.5333333333334L78.9333333333333 300.2666666666667L105.0666666666667 325.3333333333334L202.6666666666667 227.7333333333334L407.4666666666667 432.5333333333334L433.0666666666667 406.9333333333334z" /> + horiz-adv-x="512" d=" M52.2666666666667 461.3333333333333L461.3333333333333 52.2666666666667L437.3333333333333 28.8000000000001L386.6666666666667 79.4666666666667Q386.1333333333334 78.9333333333334 385.8666666666666 79.2000000000001Q385.6 79.4666666666667 385.0666666666666 79.4666666666667H115.2Q100.8 79.4666666666667 89.8666666666667 90.1333333333334T78.9333333333333 115.7333333333334V197.8666666666667H115.2V115.7333333333334H350.4L275.7333333333334 190.4L256 171.2000000000001L148.2666666666667 278.4000000000001L168 298.1333333333334L28.8 437.3333333333334zM337.6 304.5333333333334L363.7333333333334 278.4000000000001L323.7333333333334 237.8666666666667L297.6 264.5333333333334zM274.1333333333334 435.7333333333334V286.9333333333334L237.8666666666667 323.7333333333334V435.7333333333334z" /> + horiz-adv-x="512" d=" M384 168.8533333333334C367.7866666666667 168.8533333333334 353.1733333333333 162.56 342.08 152.4266666666667L190.08 241.0666666666667C191.2533333333333 245.8666666666667 192 250.8800000000001 192 256S191.2533333333333 266.1333333333334 190.08 270.9333333333334L340.48 358.7200000000001C351.8933333333333 348.0533333333334 367.1466666666667 341.44 384 341.44C419.3066666666667 341.44 448 370.1333333333334 448 405.44S419.3066666666667 469.44 384 469.44S320 440.7466666666667 320 405.44C320 400.32 320.7466666666667 395.3066666666667 321.92 390.5066666666667L171.52 302.72C160.1066666666666 313.3866666666667 144.8533333333333 320 128 320C92.6933333333333 320 64 291.3066666666666 64 256S92.6933333333333 192 128 192C144.8533333333333 192 160.1066666666666 198.6133333333334 171.52 209.28L323.52 120.64C322.4533333333333 116.16 321.8133333333333 111.4666666666666 321.8133333333333 106.6666666666666C321.8133333333333 72.3199999999999 349.6533333333333 44.48 384 44.48S446.1866666666666 72.3199999999999 446.1866666666666 106.6666666666666S418.3466666666667 168.8533333333333 384 168.8533333333333z" /> + horiz-adv-x="512" d=" M216.2048 64L208.4925866666667 125.7024C201.6823466666667 127.7546666666667 194.3182933333333 130.9802666666667 186.4004266666667 135.3856C178.48256 139.7888 171.7474133333333 144.5056000000001 166.1949866666667 149.5381333333334L109.2925866666667 125.3333333333334L69.4973866666667 194.6666666666667L118.68736 231.7546666666667C118.05824 235.6096 117.53856 239.584 117.12832 243.6714666666667C116.71808 247.7610666666667 116.5128533333334 251.7333333333334 116.5128533333334 255.5904000000001C116.5128533333334 259.1722666666667 116.71808 262.9397333333334 117.12832 266.8928C117.53856 270.8437333333334 118.05824 275.2960000000001 118.68736 280.2453333333334L69.4973866666667 317.3333333333334L109.2925866666667 385.8464L165.78496 362.0512C172.15744 367.3572266666667 179.0498133333333 372.1433600000001 186.4616533333333 376.4100266666667C193.8734933333333 380.6766933333334 201.0805333333333 383.9726933333333 208.08256 386.2976L216.2048 448H295.7952L303.5072 385.88736C311.6864 383.01568 318.9141333333334 379.7198933333334 325.1904 376C331.4666666666667 372.2805333333333 337.9285333333334 367.6309333333334 344.5738666666667 362.0512L402.7072 385.8464L442.5024 317.3333333333334L391.6714666666667 279.0144C392.8490666666667 274.6112000000001 393.504 270.5706666666667 393.6405333333333 266.8928C393.7792 263.2128 393.8474666666667 259.5818666666667 393.8474666666667 256C393.8474666666667 252.6912000000001 393.7109333333334 249.1968000000001 393.4357333333333 245.5168C393.1626666666667 241.8389333333334 392.5333333333333 237.3888 391.5498666666666 232.1642666666667L441.5594666666667 194.6666666666667L401.7642666666667 125.3333333333334L344.5738666666667 149.9477333333334C337.9285333333334 144.3690666666667 331.2405333333333 139.5818666666667 324.5141333333334 135.5904C317.7856 131.5968 310.784 128.4373333333333 303.5072 126.112L295.7952 64H216.2048zM255.4261333333334 202.6666666666667C270.304 202.6666666666667 282.912 207.8357333333333 293.2522666666667 218.1738666666667C303.5904 228.512 308.7594666666667 241.1221333333334 308.7594666666667 256C308.7594666666667 270.8778666666667 303.5904 283.4880000000001 293.2522666666667 293.8261333333334C282.912 304.1640533333334 270.304 309.3333333333334 255.4261333333334 309.3333333333334C240.4650666666667 309.3333333333334 227.8357333333334 304.1640533333334 217.5381333333334 293.8261333333334C207.2411733333333 283.4880000000001 202.0925866666667 270.8778666666667 202.0925866666667 256C202.0925866666667 241.1221333333334 207.2411733333333 228.512 217.5381333333334 218.1738666666667C227.8357333333334 207.8357333333333 240.4650666666667 202.6666666666667 255.4261333333334 202.6666666666667z" /> + horiz-adv-x="512" d=" M384 426.6666666666667H128C104.4266666666667 426.6666666666667 85.3333333333333 407.5733333333334 85.3333333333333 384V128C85.3333333333333 104.4266666666667 104.4266666666667 85.3333333333334 128 85.3333333333334H384C407.5733333333333 85.3333333333334 426.6666666666667 104.4266666666667 426.6666666666667 128V384C426.6666666666667 407.5733333333334 407.5733333333333 426.6666666666667 384 426.6666666666667zM384 128H128V384H384V128z" /> + horiz-adv-x="512" d=" M42.6666666666667 256C42.6666666666667 138.1792533694308 138.1792533694307 42.6666666666667 256 42.6666666666667C373.8207466305693 42.6666666666667 469.3333333333333 138.1792533694308 469.3333333333333 256C469.3333333333333 373.8207466305693 373.8207466305693 469.3333333333333 256 469.3333333333333C138.1792533694307 469.3333333333333 42.6666666666667 373.8207466305693 42.6666666666667 256z" /> + horiz-adv-x="512" d=" M256 469.3333333333333C138.1333333333333 469.3333333333333 42.6666666666667 373.8666666666667 42.6666666666667 256S138.1333333333333 42.6666666666667 256 42.6666666666667S469.3333333333333 138.1333333333334 469.3333333333333 256S373.8666666666666 469.3333333333333 256 469.3333333333333zM256 85.3333333333334C161.92 85.3333333333334 85.3333333333333 161.92 85.3333333333333 256S161.92 426.6666666666667 256 426.6666666666667S426.6666666666667 350.0800000000001 426.6666666666667 256S350.08 85.3333333333334 256 85.3333333333334z" /> + horiz-adv-x="512" d=" M256 469.3333333333333C138.3466666666667 469.3333333333333 42.6666666666667 373.6533333333333 42.6666666666667 256S138.3466666666667 42.6666666666667 256 42.6666666666667S469.3333333333333 138.3466666666667 469.3333333333333 256S373.6533333333333 469.3333333333333 256 469.3333333333333zM256 85.3333333333334C161.92 85.3333333333334 85.3333333333333 161.92 85.3333333333333 256S161.92 426.6666666666667 256 426.6666666666667S426.6666666666667 350.0800000000001 426.6666666666667 256S350.08 85.3333333333334 256 85.3333333333334zM320 256C320 220.6933333333334 291.3066666666666 192 256 192S192 220.6933333333334 192 256S220.6933333333333 320 256 320S320 291.3066666666667 320 256z" /> + horiz-adv-x="512" d=" M256 469.3333333333333C138.1333333333333 469.3333333333333 42.6666666666667 373.8666666666667 42.6666666666667 256S138.1333333333333 42.6666666666667 256 42.6666666666667S469.3333333333333 138.1333333333334 469.3333333333333 256S373.8666666666666 469.3333333333333 256 469.3333333333333zM362.6666666666667 179.52L332.48 149.3333333333334L256 225.8133333333334L179.52 149.3333333333334L149.3333333333333 179.52L225.8133333333334 256L149.3333333333333 332.48L179.52 362.6666666666667L256 286.1866666666667L332.48 362.6666666666667L362.6666666666667 332.48L286.1866666666666 256L362.6666666666667 179.52z" /> + horiz-adv-x="512" d=" M149.3333333333333 362.6666666666667H362.6666666666667V298.6666666666667L448 384L362.6666666666667 469.3333333333333V405.3333333333334H106.6666666666667V277.3333333333334H149.3333333333333V362.6666666666667zM362.6666666666667 149.3333333333334H149.3333333333333V213.3333333333334L64 128L149.3333333333333 42.6666666666667V106.6666666666667H405.3333333333333V234.6666666666667H362.6666666666667V149.3333333333334z" /> + horiz-adv-x="512" d=" M256 405.3333333333334V490.6666666666667L149.3333333333333 384L256 277.3333333333334V362.6666666666667C326.72 362.6666666666667 384 305.3866666666667 384 234.6666666666667S326.72 106.6666666666667 256 106.6666666666667S128 163.9466666666667 128 234.6666666666667H85.3333333333333C85.3333333333333 140.3733333333333 161.7066666666667 64 256 64S426.6666666666667 140.3733333333333 426.6666666666667 234.6666666666667S350.2933333333334 405.3333333333334 256 405.3333333333334z" /> + horiz-adv-x="512" d=" M188.6775466666667 490.6666666666667L95.9166933333333 397.9058133333333L188.6775466666667 305.1448533333333L215.2018133333334 331.64448L169.1294933333334 377.7168000000001H183.0078933333333C235.59616 377.7168000000001 279.7704533333333 359.3499733333333 315.5304533333333 322.6222933333334C351.29056 285.8946133333334 369.17056 241.1553066666667 369.17056 188.3988266666667H407.03424C407.03424 219.9517866666667 401.1394133333334 249.51136 389.3595733333333 277.0676266666667C377.57984 304.6238933333333 361.38752 328.7044266666667 340.7729066666667 349.3190400000001C320.1582933333333 369.9336533333334 296.07776 386.1259733333333 268.5214933333333 397.9058133333333C240.9652266666667 409.6855466666666 211.4055466666667 415.5803733333333 179.8525866666667 415.5803733333333H165.3333333333333L214.5608533333333 464.8079999999998zM102.4984533333333 259.6395733333334V111.7842133333334H223.6324266666667V58.3659733333334H102.4984533333333V10.6666666666667H237.9544533333334C246.8574933333334 10.6666666666667 254.4957866666667 13.8472533333334 260.8550400000001 20.2065066666668C267.2144 26.5658666666668 270.39488 34.1794133333333 270.39488 43.0824533333334V126.0816000000002C270.39488 134.9846400000003 267.2144 142.6229333333334 260.8550400000001 148.9821866666668C254.4957866666667 155.3414400000002 246.8574933333334 158.5220266666669 237.9544533333334 158.5220266666669H150.19776V212.8770133333336H270.39488V259.6395733333336z" /> + horiz-adv-x="512" d=" M256.0384 38.8437333333333C228.9216 38.8437333333333 203.5050666666667 43.9061333333333 179.79136 54.0330666666667C156.0772266666667 64.1578666666667 135.3501866666667 78.0821333333333 117.6100266666667 95.8016C99.8696533333333 113.5232 85.9466666666667 134.2186666666667 75.8414933333333 157.8901333333333C65.73632 181.5594666666667 60.68352 206.9845333333334 60.68352 234.1610666666667H97.02016C97.02016 189.9712 112.4744533333333 152.4501333333334 143.3832533333333 121.5936000000001C174.2920533333334 90.7370666666667 211.82336 75.3088 255.9765333333334 75.3088C300.096 75.3088 337.6042666666667 90.7520000000001 368.5034666666666 121.6405333333333C399.4026666666667 152.5290666666667 414.8522666666667 190.0352 414.8522666666667 234.1610666666667C414.8522666666667 278.3488 399.8549333333334 315.8709333333334 369.8624 346.7274666666667C339.8698666666667 377.584 302.7626666666667 393.0122666666667 258.5386666666667 393.0122666666667H246.8053333333334L284.9728 354.8443733333334L261.4250666666667 331.4240000000001L180.6024533333333 412.37504L261.4250666666667 493.1979093333333L284.4394666666667 470.1834666666667L243.6053333333334 429.4762666666667H255.872C283.0336 429.4762666666667 308.4757333333333 424.4136533333334 332.1962666666667 414.288C355.9189333333334 404.1621333333334 376.64 390.2393600000001 394.3594666666667 372.5194666666667C412.0789333333333 354.7995733333334 426.0010666666667 334.09728 436.128 310.4123733333334C446.2528 286.7264 451.3152 261.3248 451.3152 234.2037333333334C451.3152 207.0826666666667 446.2634666666667 181.6704 436.1557333333334 157.9648C426.048 134.2613333333334 412.1237333333334 113.536 394.3808 95.7888C376.6378666666667 78.0437333333334 355.9253333333334 64.1152000000001 332.2410666666667 54.0074666666667C308.5589333333333 43.8976 283.1573333333334 38.8437333333333 256.0384 38.8437333333333zM190.34176 163.9338666666667V277.2565333333333H162.1796266666667V304.0266666666667H218.1568V163.9338666666667H190.34176zM246.4234666666667 163.9338666666667V304.0266666666667H340.0469333333333V163.9338666666667H246.4234666666667zM274.3658666666667 191.2384H312.2325333333333V277.2565333333333H274.3658666666667V191.2384z" /> + horiz-adv-x="512" d=" M277.8333333333333 501.3333333333333L184.6874986666667 408.18752L277.8333333333333 315.0417066666666L304.458336 341.6458666666666L258.2083306666667 387.9166933333333H272.1458346666667C324.953824 387.9166933333333 369.2988693333333 369.46432 405.2083306666667 332.58336C441.1177066666667 295.7022933333333 459.083328 250.7894399999999 459.083328 197.8124799999999H497.1041706666666C497.1041706666666 229.49728 491.1831253333333 259.1620266666666 479.3541653333333 286.8332799999999C467.5252053333334 314.5046399999998 451.263264 338.6951466666665 430.562496 359.3958399999999C409.861824 380.0965333333333 385.6713493333333 396.3585066666665 358 408.1875199999999C330.32864 420.0165333333332 300.663872 425.9374933333333 268.979168 425.9374933333333H254.3958293333333L303.8333333333333 475.3750399999999zM27.25 282.3333333333334V232.3333333333334H137.083328V176.3333333333334H62.6458336V126.3333333333334H137.083328V71.3333333333334H27.25V21.3333333333334H142.520832C153.8368853333333 21.3333333333334 163.3242346666667 25.5461333333333 170.979168 33.9792000000001C178.6341013333333 42.4121600000002 182.458336 52.8879999999999 182.458336 65.3541333333333V238.3333333333334C182.458336 250.7994666666667 178.6341013333333 261.2545066666667 170.979168 269.6874666666667C163.3242346666667 278.1204266666666 153.8368853333333 282.3333333333334 142.520832 282.3333333333334zM267.062496 280.83328C254.8071786666667 280.83328 244.5493013333334 276.67424 236.2916693333333 268.4166399999999C227.9685546666667 260.0935466666667 223.7916693333333 249.8177066666666 223.7916693333333 237.5624533333334V67.4999466666665C223.7916693333333 55.2447999999998 227.9299733333333 44.9868799999998 236.1874986666667 36.7291733333332C244.51072 28.4060799999998 254.8071786666667 24.22912 267.062496 24.22912H348.6458346666667C360.901152 24.22912 371.15904 28.3672533333332 379.4166613333333 36.6249600000001C387.7397973333333 44.9480533333335 391.8958293333333 55.2448000000001 391.8958293333333 67.4999466666668V237.5624533333335C391.8958293333333 249.8177066666667 387.7783573333334 260.0756266666667 379.520832 268.3333333333335C371.1977066666667 276.6564266666668 360.901152 280.8332800000002 348.6458346666667 280.8332800000002zM272.958336 232.6458666666666H342.7708373333333V73.3958399999999H272.958336z" /> + horiz-adv-x="512" d=" M314.7565866666667 501.3333333333333L288.816 475.4173866666667L338.1525333333333 426.0808533333334H323.6010666666666C291.97824 426.0808533333334 262.35328 420.1730133333334 234.736 408.3671466666669C207.11872 396.5612800000001 182.9848533333333 380.3331200000001 162.3245866666667 359.6729600000002C141.6644266666667 339.0126933333336 125.4362666666667 314.8788266666669 113.6304 287.261546666667C101.8245333333333 259.6443733333336 95.9166933333333 230.0193066666669 95.9166933333333 198.3964800000004H133.8641066666667C133.8641066666667 251.2697600000003 151.78368 296.1081600000004 187.6229333333333 332.9170133333337C223.46208 369.7259733333337 267.73408 388.1334400000004 320.4388266666666 388.1334400000004H334.3479466666667L288.1982933333333 341.9592533333338L314.7565866666667 315.4009600000004L407.7227733333333 408.3671466666671zM210.4011733333333 270.8325333333334V122.6746666666668H331.8032V69.1381333333335H210.4011733333333V21.3333333333334H346.1570133333333C355.0797866666666 21.3333333333334 362.7101866666666 24.5209600000002 369.0836266666666 30.8942933333335C375.4569599999999 37.2677333333335 378.6445866666666 44.8981333333335 378.6445866666666 53.8208000000001V137.0037333333334C378.6445866666666 145.9265066666667 375.45696 153.5816533333334 369.0836266666666 159.9549866666666C362.7101866666666 166.32832 355.0797866666666 169.5159466666668 346.1570133333333 169.5159466666668H258.20608V223.9912533333335H378.6445866666666V270.8325333333334z" /> + horiz-adv-x="512" d=" M190.34176 163.9338666666667V277.2565333333333H162.1796266666667V304.0266666666667H218.1568V163.9338666666667H190.34176zM246.4234666666667 163.9338666666667V304.0266666666667H340.0469333333333V163.9338666666667H246.4234666666667zM274.3658666666667 191.2384H312.2325333333333V277.2565333333333H274.3658666666667V191.2384zM256.0426666666667 38.8437333333333C228.9216 38.8437333333333 203.50976 43.8976 179.8052266666667 54.0053333333334C156.1002666666667 64.1130666666667 135.37472 78.0394666666667 117.6285866666667 95.7845333333333C99.8824533333333 113.5274666666667 85.9552 134.2421333333334 75.8468266666667 157.9264C65.7380266666667 181.6106666666667 60.68352 207.0144 60.68352 234.1333333333334C60.68352 261.2885333333334 65.74656 286.7136000000001 75.8724266666667 310.40384C85.9978666666667 334.0945066666667 99.92064 354.7995733333334 117.64032 372.5194666666667C135.3602133333333 390.2393600000001 156.0808533333333 404.1621333333334 179.8024533333334 414.288C203.52384 424.4136533333334 228.9664 429.4762666666667 256.128 429.4762666666667H267.328L226.6197333333334 470.1834666666667L249.5061333333334 493.1979093333333L330.3296 412.37504L249.5061333333334 331.4240000000001L226.6197333333334 354.31104L265.3205333333333 393.0122666666667H256.128C211.9381333333333 393.0122666666667 174.37312 377.5837866666667 143.4318933333333 346.72704C112.4906666666667 315.8700800000001 97.02016 278.3680000000001 97.02016 234.2229333333334C97.02016 190.0778666666667 112.4744533333333 152.5546666666667 143.3832533333333 121.6554666666667C174.2920533333334 90.7584000000001 211.82336 75.3088 255.9765333333334 75.3088C300.1301333333334 75.3088 337.6704 90.7370666666667 368.5930666666667 121.5936000000001C399.5178666666667 152.4501333333334 414.9802666666667 189.9712 414.9802666666667 234.1610666666667H451.3152C451.3152 206.9845333333334 446.2634666666667 181.5594666666667 436.1578666666667 157.8901333333333C426.0522666666667 134.2186666666667 412.1301333333334 113.5232 394.3893333333333 95.8016C376.6506666666667 78.0821333333333 355.936 64.1578666666667 332.2517333333333 54.0330666666667C308.5653333333334 43.9061333333333 283.1637333333333 38.8437333333333 256.0426666666667 38.8437333333333z" /> + horiz-adv-x="512" d=" M272.5187306666667 501.3333333333333L246.527296 475.3666133333333L295.9605440000001 425.93344H281.3805866666667C249.695808 425.93344 220.012704 420.0139733333333 192.341344 408.18496C164.6699733333333 396.3559466666666 140.4888 380.0959999999999 119.788096 359.3953066666667C99.0873706666667 338.6946133333333 82.827424 314.5134933333333 70.9984426666667 286.8420266666667C59.1694613333334 259.1706666666668 53.2500053333334 229.4875733333334 53.2500053333334 197.8028800000001H91.2717653333334C91.2717653333334 250.7797333333335 109.2265173333334 295.7059200000001 145.135936 332.5869866666667C181.045344 369.4680533333334 225.4041493333334 387.9116800000001 278.2121066666667 387.9116800000001H292.1484693333333L245.908448 341.6468266666668L272.5187306666667 315.0365866666668L365.667104 408.1849600000001zM149.29456 282.3368533333334V232.3342933333334H259.1269546666666V176.3413333333333H184.6924266666667V126.3387733333333H259.1269546666666V71.3358933333333H149.29456V21.3333333333334H264.5727786666667C275.8888213333334 21.3333333333334 285.359872 25.5495466666668 293.0148373333334 33.9825066666666C300.6698026666667 42.4154666666667 304.5005866666667 52.8793599999999 304.5005866666667 65.3454933333333V238.3246933333334C304.5005866666667 250.7908266666667 300.6698026666667 261.2547200000001 293.0148373333334 269.68768C285.359872 278.12064 275.8888213333334 282.3368533333334 264.5727786666667 282.3368533333334zM375.1477973333334 280.5546666666668C362.8924693333334 280.5546666666668 352.63648 276.4105600000002 344.3788800000001 268.1529600000001C336.055744 259.8298666666668 331.87824 249.5403733333335 331.87824 237.2850133333333V67.2267733333332C331.87824 54.9714133333331 336.0222613333334 44.7155199999997 344.2798613333333 36.4579199999998C352.6029973333333 28.1348266666664 362.8924693333334 23.9819733333331 375.1477973333334 23.9819733333331H456.7361066666667C468.9914666666667 23.9819733333331 479.2474666666667 28.1013333333331 487.5050666666667 36.3588266666663C495.8281600000001 44.6820266666664 499.9809066666667 54.9714133333331 499.9809066666667 67.2267733333329V237.2850133333329C499.9809066666667 249.5403733333328 495.8616533333334 259.7963733333329 487.6040533333334 268.0539733333329C479.2809600000001 276.3770666666662 468.9914666666667 280.5546666666663 456.7361066666667 280.5546666666663zM381.0391786666667 232.3837866666668H450.8200533333334V73.1181866666669H381.0391786666667z" /> + horiz-adv-x="512" d=" M256 490.6666666666667C149.9733333333333 490.6666666666667 64 404.6933333333334 64 298.6666666666667V149.3333333333334C64 114.0266666666667 92.6933333333333 85.3333333333334 128 85.3333333333334H192V256H106.6666666666667V298.6666666666667C106.6666666666667 381.12 173.5466666666667 448 256 448S405.3333333333333 381.12 405.3333333333333 298.6666666666667V256H320V85.3333333333334H384C419.3066666666667 85.3333333333334 448 114.0266666666667 448 149.3333333333334V298.6666666666667C448 404.6933333333334 362.0266666666667 490.6666666666667 256 490.6666666666667z" /> + horiz-adv-x="512" d=" M128 128L309.3333333333333 256L128 384V128zM341.3333333333333 384V128H384V384H341.3333333333333z" /> + horiz-adv-x="512" d=" M128 384H170.6666666666667V128H128zM202.6666666666667 256L384 128V384z" /> + horiz-adv-x="512" d=" M225.8133333333334 316.3733333333334L115.52 426.6666666666667L85.3333333333333 396.48L195.6266666666667 286.1866666666667L225.8133333333334 316.3733333333334zM309.3333333333333 426.6666666666667L352.9600000000001 383.04L85.3333333333333 115.52L115.52 85.3333333333334L383.1466666666667 352.9600000000001L426.6666666666667 309.3333333333334V426.6666666666667H309.3333333333333zM316.3733333333334 225.8133333333334L286.1866666666666 195.6266666666667L352.9599999999999 128.8533333333334L309.3333333333333 85.3333333333334H426.6666666666667V202.6666666666667L383.04 159.04L316.3733333333333 225.8133333333333z" /> + horiz-adv-x="512" d=" M448 448H64C40.4266666666667 448 21.3333333333333 428.9066666666667 21.3333333333333 405.3333333333334V341.3333333333334H64V405.3333333333334H448V106.6666666666667H298.6666666666667V64H448C471.5733333333333 64 490.6666666666666 83.0933333333334 490.6666666666666 106.6666666666667V405.3333333333334C490.6666666666666 428.9066666666667 471.5733333333333 448 448 448zM21.3333333333333 128V64H85.3333333333333C85.3333333333333 99.3066666666667 56.64 128 21.3333333333333 128zM21.3333333333333 213.3333333333334V170.6666666666667C80.2133333333333 170.6666666666667 128 122.88 128 64H170.6666666666667C170.6666666666667 146.4533333333334 103.7866666666667 213.3333333333334 21.3333333333333 213.3333333333334zM21.3333333333333 298.6666666666667V256C127.36 256 213.3333333333333 170.0266666666667 213.3333333333333 64H256C256 193.6 150.9333333333333 298.6666666666667 21.3333333333333 298.6666666666667z" /> + horiz-adv-x="512" d=" M42.6666666666667 426.6666666666667H341.3333333333333H469.3333333333333V384V256H426.6666666666667V384H341.3333333333333H85.3333333333333V213.3333333333334V128H213.3333333333333V85.3333333333334H42.6666666666667V128V213.3333333333334V384V426.6666666666667zM469.3333333333333 85.3333333333334V128V213.3333333333334H426.6666666666667H256V85.3333333333334H469.3333333333333z" /> + horiz-adv-x="512" d=" M298.6666666666667 426.6666666666667H341.3333333333333H469.3333333333333V384V213.3333333333334V85.3333333333334H426.6666666666667H42.6666666666667V128V213.3333333333334V256H85.3333333333333V213.3333333333334V128H426.6666666666667V213.3333333333334V384H341.3333333333333H298.6666666666667V426.6666666666667zM42.6666666666667 426.6666666666667H256V298.6666666666667H42.6666666666667V426.6666666666667z" /> - + - - - - + horiz-adv-x="512" d=" M330.6666666666667 213.3333333333334H313.7066666666667L307.84 219.2000000000001C328.7466666666667 243.4133333333334 341.3333333333333 274.88 341.3333333333333 309.3333333333334C341.3333333333333 385.92 279.2533333333334 448 202.6666666666667 448S64 385.92 64 309.3333333333334S126.08 170.6666666666667 202.6666666666667 170.6666666666667C237.12 170.6666666666667 268.5866666666667 183.2533333333333 292.8 204.0533333333334L298.6666666666667 198.1866666666667V181.3333333333334L405.3333333333333 74.88L437.12 106.6666666666667L330.6666666666667 213.3333333333334zM202.6666666666667 213.3333333333334C149.6533333333333 213.3333333333334 106.6666666666667 256.3200000000001 106.6666666666667 309.3333333333334S149.6533333333333 405.3333333333334 202.6666666666667 405.3333333333334S298.6666666666667 362.3466666666667 298.6666666666667 309.3333333333334S255.68 213.3333333333334 202.6666666666667 213.3333333333334z" /> + unicode="" + horiz-adv-x="512" d=" M227.289682989 130.648838L227.289682989 349.0070938950001C277.6702185 350.0243205440001 316.3980869 352.03531578 344.921102 331.8810063C372.295426 312.5387722000001 393.654923 274.9158875 389.810661 226.556463C385.743598 175.400839 348.475879 132.417149 300.0353987 128.081247C275.9502648 125.925665 228.0625443 128.081247 228.0625443 128.081247C228.0625443 128.081247 227.222611733 129.433284 227.289682989 130.648838M276.0447042 185.453256C307.4745261 184.193345 329.253026 202.086298 332.540288 230.838625C336.942707 269.3482243000001 311.3515531 294.3911928 274.4985961 291.6350844L274.4985961 188.02298C274.4627477 186.556638 275.0744838 185.803848 276.0447042 185.453256 M383.779991 130.671935C397.1159924 126.962576 401.166865 141.7313420000001 406.9951259 152.079757C419.716693 174.6511000000001 429.2886092 203.673834 429.4404832 236.853763C429.6613558 284.4480278 411.0972656 319.3706768 393.839523 343.0355910887L385.32918283 343.0355910887C384.74288754 339.1624526400001 387.55564016 335.7213689500001 389.19541674 332.7596826C402.3614272 308.9983771000001 414.5618429 276.2065726 414.7356885 238.566201C414.9345895 195.3901550000001 400.5732459 159.287779 383.779991 130.671935 M425.153705 130.671935C438.4897064 126.962576 442.5378808 141.733475 448.3688399 152.079757C461.090407 174.6511000000001 470.6603959 203.673834 470.8141972 236.853763C471.0350698 284.4480278 452.4709796 319.3706768 435.213237 343.0355910887L426.70289683 343.0355910887C426.11660154 339.1624526400001 428.92858323 335.7205159300001 430.56913074 332.7596826C443.7351412 308.9983771000001 455.9359424 276.2065726 456.1094025 238.566201C456.3083035 195.3901550000001 441.9469599 159.287779 425.153705 130.671935 M466.260868 130.671935C479.5968694 126.962576 483.6450438 141.733475 489.4760029 152.079757C502.19757 174.6511000000001 511.7675589 203.673834 511.9213602 236.853763C512.1422328 284.4480278 493.5781426 319.3706768 476.3204 343.0355910887L467.81005983 343.0355910887C467.22376454 339.1624526400001 470.03574623 335.7209424400001 471.67629374 332.7596826C484.8423042 308.9983771000001 497.0427199 276.2065726 497.2165655 238.566201C497.4154665 195.3901550000001 483.0541229 159.287779 466.260868 130.671935 M4.4765625 128.994842L72.5800993 128.994842L91.1530552 157.478514L155.321745 157.613942C155.321745 157.613942 155.386889 138.200917 155.386889 128.994842L204.142681 128.994842L204.142681 351.691737L145.326586 351.691737C139.673713 342.1546170000001 4.4765625 128.994842 4.4765625 128.994842L4.4765625 128.994842zM157.144233 274.277389L157.144233 203.118942L116.6914 203.389797L157.144233 274.277389L157.144233 274.277389z" /> + unicode="" + horiz-adv-x="512" d=" M149.3333333333333 128C125.76 128 106.88 108.9066666666667 106.88 85.3333333333334S125.76 42.6666666666667 149.3333333333333 42.6666666666667S192 61.76 192 85.3333333333334S172.9066666666667 128 149.3333333333333 128zM21.3333333333333 469.3333333333333V426.6666666666667H64L140.6933333333333 264.8533333333334L111.8933333333334 212.5866666666667C108.5866666666667 206.4 106.6666666666667 199.4666666666667 106.6666666666667 192C106.6666666666667 168.4266666666667 125.76 149.3333333333334 149.3333333333334 149.3333333333334H405.3333333333333V192H158.4C155.4133333333333 192 153.0666666666667 194.3466666666667 153.0666666666667 197.3333333333334C153.0666666666667 198.2933333333334 153.28 199.1466666666667 153.7066666666667 199.8933333333333L172.8 234.6666666666667H331.7333333333334C347.7333333333334 234.6666666666667 361.7066666666667 243.52 369.0666666666667 256.64L445.3333333333333 395.0933333333334C447.04 398.08 448 401.6 448 405.3333333333334C448 417.1733333333334 438.4 426.6666666666667 426.6666666666667 426.6666666666667H111.2533333333333L90.9866666666667 469.3333333333333H21.3333333333333zM362.6666666666667 128C339.0933333333333 128 320.2133333333333 108.9066666666667 320.2133333333333 85.3333333333334S339.0933333333333 42.6666666666667 362.6666666666667 42.6666666666667S405.3333333333333 61.76 405.3333333333333 85.3333333333334S386.24 128 362.6666666666667 128z" /> + unicode="" + horiz-adv-x="512" d=" M192 166.3978666666667L102.4 255.9978666666667L72.5333333333333 226.1312000000001L192 106.6645333333333L448 362.6645333333334L418.1333333333334 392.5312L192 166.3978666666667z" /> diff --git a/src/assets/icon-font/VideoJS.ttf b/src/assets/icon-font/VideoJS.ttf index 1969881c393c5542aa331f66e2f5733d176bb1ce..53b10bc14df393fdec9e87fbde65431096a49ca1 100755 GIT binary patch literal 7220 zcmd^Edu&_hb^p%g<5Cn!i6SqrNJ$jM*QG>}lK2p9DUk@rkIivx#SRrGi?h%YWyz#S zMWSpc%?bj|)^u^PV2g~b3sz$&iqz_`wP=Q7?FI!%yB0|DM^+nYfvtPwpUpsl+r?Y| zqWAmmz4WUI`u8wW=ey@S-}{{3Ip@0+BgR-aYcUg>JaVdZAoObNMaF0!q)(hUHIXTP z`?2Ryzl{3g(t3I0lgoQwW=wdKG1EuqYA>w3`^(T0W7eNA*89n7rMx`)@Zm$~`!AsB zRS3)`Ar1cfpy=v)^U~8`FyF^0zFTW7m7l9WQ^WWs>d&s1FKy5~!+g|_dZ=DruU!9! zTUQyAe}pyd8;z~ze{KGDhA}_Z6P{z(yMT^*dHc3NTyozhtw|t#_j6zPtKaQ;=oahN zM||8Pe1rGwJO!fvM%89ah%KO&W=}Ga_tKwIiil^cu53e2`WkJ7PRti0~WfJo-ne{{C0XDz}*$@k|VHRQ| zEX*P-%3>_e6qaD4Y>XwbehLe4`~83aIJg1?ZYTbMR#a{WZX^B#^f++{s<0;~ZdVnS z;lwSZ!q%L)%~fcW6ZZ@i9>|G%q6+`v#QjL^(g~JiDlou_V?piK3D#vQcEt&HW~x&s zSe&UYonU*W_UHr@Ftt}F;DM=boq!Cc_UQzSFx8_I(85%&PQVRQC7pmEruuXOmY6E* z1XM8$}s`r*ucku$a#{z#lFivBpan^k=~>|!7D5XmxP}PzcRT@51BSi zd!~;~ZS%N!#r(SYJLZ43^ja1yZ(2UEx~xxIU$K70`c3PHqF0<0kBKYdo3@Z`)yAU$ z5Qk!yb~)Y(Zp#k&s#M(Nh=cqF-GID@>vW!!LfA##AZ6w#mnKs#bID z=Is9cXPdm3NuNlkPdvkmtIs{RZAj77ZW&^~{grjDYJ=|-xwSSdZA)1n1u75fgqAQ_&J(5?lbX#p*=4Y!<(yKGQ z6TPX48K=L`9UhXy(n3x-@r+p@Av5ljrRZ9yeLl3$DR!Br%!1i$diFxK#kZ;LbrJf6 zZ+fJx>=t(x&!4Aat4fR2&YAy6Ylqhr^4Wr0DL{!B%D1b;`j$FO17w2N+wHSxU|%|cP|+HxIl%#J$T?h)2ReK`c#*C`aTu%I7SKizKg{6@w%f zvZ61W0S10tk>&WI!Q*7Bk5_90L+yV(H85np`e$4aIb}45{@UV~zZjl4;{Kxa zy`iI{lJpuceG-?v#@qC4%V^KQZZ~`cF|ObqamR2b8KfA`BDLD@z4g|IJ|F&yZ@pFJ zpgtx^Qo-j#$kYAo9pIq{QDYwG8wpz_x7U{)$xP?nxwsg~bCqW$uQf^G_`{T&o~4XW zl0QSv{ie_F@1rr4-w!2|A!k>y%i%!b+~HM?(`2;k@4L;k&M2>R7rT8nr_;8`3*UXU zyS97>`y4>rLnM1}rzXCm2zHvy&k7me5HOvj0@U!}mO`OqXiQsW5gVdCf8X6L-)6~V zE1%bP5h`|UzDHFpM(BD*cHq-;^yImgNAe>QD379;)IKFjwy*1SbF7ub7v*jeVU}k~ zMn9#WA)m>JB?_Bh1o2?5S07nFjGV-#dDKSu@GMIIn440XT z|1o+b7i>K7$dgYVkZXbY_}oBA9`^+Ovjy+C9GVFPhkD$mF>}Amefjs7yB?_x%)j>9{J=+}rarXyQ`zM)Cro}%`~4x$cz4F<{K| zZTc4UiQ7N{s^g;z@*1Vzdns`Cy}&{&R{os7{C>c|<_Giv;((9mTMuGtcxpsnJFAFF zL9xi<1W8IhM}OL%p|*+*+&t>7d0(Y`AQ9+~();ama)xi?sHf_E_2q%Y!0t|L!-$i6 z@Z~ULv9PF2=ahmkGo6bA{$5$c!|TfwWK=XMwfg(s5QHn9Y&@Qe>(8v`4I!F~M!(^H z%I312?u+MRu>ny$?M4sxX`3|=i)E3ajWd4_F-^f+2rF>&@x4Dx+|qM2Pg$AA30d^X z5^|(0B6{?YqVLA*`^aXOyY?qCf{;n7cJd(t1WieZ*!REUL77E#^0U2@%*brkYDqu5%&lo zD0ZBbJ1kz^?sHrq9gxOcdThk~&2uM9BV#A;=3ioWlk}anc|3LSn-zU#dcSwvJ;|M} z-<|q_)NWv>GdFDf*|QhC)^I#Go$-CfGJP;7{yuB9-2iXJ?aQ#Uz9#b4eJgrI&Ey>E z;=Suk4Ieu;{Mch!dEW{@??};hXKs#H8^7gx5FQk11o=IZ7iGkr$j*&Y={6g?fd{cM zsuoM7VymT<7;e~Smu~VbNPLWbbHXhuAGu>LV%2J7HXNQkJR43LcO}|ctX3Bzvxm4m zGI2beK7Qsnzdjk~eFJm$axe0Th+J8uhHNe^Ao7Y5vaE#6hnx{lB4g+O?#!7$+I6BD zkIbE#=*c|);)}01Js#)jOLX(hnSpeDVe;f$bn5-rrzbqlQ%>*nL?`aQOTWNfIs*Se z2#tl)LO!dzWs=}5P-!+36ud4$;Sm{eIVM{M*+J{Udo@|mX(IO6>a3v$B2CBgiW z>fpY?XAi{V3i#0jXV4|y|M=tbD5{VDRzCQ9&ktO_e0gB8v;HsfivvGnhD}3z51iAY zpY!fnBXZb^?Bhe8)Xp9ep$PKnhp*xAPvG$FoAskd>rJ=M=YENM*GXO+JA9aKaaA-< zOq_x$-pTCC6f%Lnzx4RY-?z)VpqRXRSNHn9_?{wRmjKlxJ7a&pu6vpdOt~gc%|+tX z6Rz~YnIA&=E|14``UScbn1Vqj7wQmy>GkPJk4sm+ei!4qw;)zKx$X=1DOiLha&y8g zvJp1}Zb95Cu#HRjdqcr}hQdd?6EX}C@6wbVg`U4===c}lPr@prG7O-=5@!zZF~FC; z{wELU{?pF+n{@9EO!aVu#%tQyf2ylqmfiLHRUep&0+}ZoP{il&RlVmL?|04PpZY5H zrN_E=fO~!yM%2=c4b|u`Btz7JeVT~nPUqr*VkvR(MUi(@n^(1me5N#GwTK zlDqN16^Jmk;ZrSYfx}$^Z#`Le*4=vh?d!=_3xSxgt>acb0jPjq<4^qWxS&T2ZUSV5M}ctZtbZ2qDTNA-T@F+cmSZg9PPDRef}i1 zc6Ok}_9MmEz|hoCa`fP6?x}2YzQ!+L#Uifp`lYeG2PKup&}H;sVlfH56l?RwnQ!1c zc4R8%8R0kh5jV<_hoSByjkU(w*IL);+I8C9g%%0~MqC;NjYQ+Tj0T1*y` zkD^^B3fM)f$!wCNHZqgnZnNC2v^|IVic_g2~w%7z!)J*&Ig0%HmPK|%g2yc@ zqbVR4kBlhP>9*tc`y+n;A15ChviRM^{zov8=c^8n^%!na?RUM@3(%rNix&|;mB#)e zXYU4{O=gNX?0;ynbUDY`yLxK}H+%FY#7x}uM46Pmr-v>Q3x56SAb;>pBbWS8M;J;G zbq#I6kBG0%^Is%f25$y089aZJ9W;3U>!@Pzc6g=!3p)QFfrmW>&5*eZtu9tGcme#2 z25$oYiou(~^WQ*te+zEfUo&_sL27twQ*ZDDkS)mey5K}VAh zj5vZmb**2SHSt_Vzq9NGlw0fq*2zHHWGkqzV6FyR2X|PTkN2+O+kj+4Yda6g5_+fE zsy1o^emQ~f$^-q==*4F;VmY2b%cp+QB#;RXWg#nx`1P%e?9@#TauR<<=p{Gxk%zn_ zkq@!GpZpY{0fK)}kcKHlBNV0xMJYycQYb;AG)74prxbq2n_%XRTKR=;UZj@RHkWD@ z%SL&7tI~a;QQKaxq}I2a6}!RL8W%h4`r2}*$LiX-)!vouT5W4-vr?(2Dlq0|&%J_6 zYfW)$W33MRcWrH-ZLT$Il`Y#+d7}x0YzeE&wxv~6fSB&(#>INAQC?oFpL4FP)helu zpzppK#Py2%eo?p-mI9G8t1HA&u@dZ>PK?vk5ps3SzD`DJVsU9k#TD4Qn|iV zsaZEG8-*-Ub)mv ztu-s_&W+8=g|)`^mR1wDR<~DHY8CTRd8^sKv9{FQ-mIk7>ZuN^sZs9K?rnfNElah= hR>itiDQ_;Vy0wK;%M}=4P17kR-9(?&%2JiF{{@MG+ZzA? literal 9200 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 { + const button = videojs.dom.createEl('button', { + className: 'vjs-visual-search-button', + title: 'Search video content' + }); + + // Add search icon + const iconElement = videojs.dom.createEl('span', { + className: 'vjs-icon-search' + }); + button.appendChild(iconElement); + + button.addEventListener('click', onClick); + + return button; +}; diff --git a/src/plugins/visual-search/components/SearchInput.js b/src/plugins/visual-search/components/SearchInput.js new file mode 100644 index 000000000..4f969bb56 --- /dev/null +++ b/src/plugins/visual-search/components/SearchInput.js @@ -0,0 +1,49 @@ +import videojs from 'video.js'; + +export const SearchInput = (onSearch, onClose) => { + const form = videojs.dom.createEl('form', { + className: 'vjs-visual-search-form' + }); + + const input = videojs.dom.createEl('input', { + className: 'vjs-visual-search-input', + type: 'text' + }); + + const closeButton = videojs.dom.createEl('button', { + className: 'vjs-visual-search-close', + type: 'button', + title: 'Close search' + }); + + // Add close icon + const closeIcon = videojs.dom.createEl('span', { + className: 'vjs-icon-close' + }); + closeButton.appendChild(closeIcon); + + form.appendChild(input); + form.appendChild(closeButton); + + // Handle search submission + form.addEventListener('submit', (e) => { + e.preventDefault(); + const query = input.value.trim(); + if (query) { + onSearch(query); + } + }); + + // Handle close button + closeButton.addEventListener('click', (e) => { + e.preventDefault(); + if (onClose) { + onClose(); + } + }); + + return { + element: form, + input + }; +}; diff --git a/src/plugins/visual-search/components/SearchResults.js b/src/plugins/visual-search/components/SearchResults.js new file mode 100644 index 000000000..347ee4664 --- /dev/null +++ b/src/plugins/visual-search/components/SearchResults.js @@ -0,0 +1,45 @@ +import videojs from 'video.js'; + +export const SearchResults = (player) => { + const clearMarkers = () => { + player.$$('.vjs-visual-search-marker').forEach(el => el.remove()); + + // Remove the class that indicates search results are displayed + player.removeClass('vjs-visual-search-results-active'); + }; + + const displayResults = (results) => { + // Clear existing markers + clearMarkers(); + + const total = player.duration(); + const seekBar = player.controlBar.progressControl.seekBar; + + // Add markers for each result + results.forEach(result => { + const { startTime, endTime } = result; + const markerEl = videojs.dom.createEl('div', { + className: 'vjs-visual-search-marker', + style: `left: ${(startTime / total) * 100}%; width: ${((endTime - startTime) / total) * 100}%` + }); + + seekBar.el().appendChild(markerEl); + + // Add click handler to jump to this time + markerEl.addEventListener('click', () => { + player.currentTime(startTime); + player.play(); + }); + }); + + // Add a class to indicate search results are displayed + if (results.length > 0) { + player.addClass('vjs-visual-search-results-active'); + } + }; + + return { + displayResults, + clearMarkers + }; +}; diff --git a/src/plugins/visual-search/index.js b/src/plugins/visual-search/index.js new file mode 100644 index 000000000..bab095131 --- /dev/null +++ b/src/plugins/visual-search/index.js @@ -0,0 +1,9 @@ +export default async function lazyVisualSearchPlugin(options) { + const player = this; + try { + const { default: initPlugin } = await import(/* webpackChunkName: "visual-search" */ './visual-search'); + player.ready(() => initPlugin(options, player)); + } catch (error) { + console.error('Failed to load plugin:', error); + } +} diff --git a/src/plugins/visual-search/utils/mockData.js b/src/plugins/visual-search/utils/mockData.js new file mode 100644 index 000000000..6dd9f2616 --- /dev/null +++ b/src/plugins/visual-search/utils/mockData.js @@ -0,0 +1,54 @@ +/** + * Generates mock search results for the visual search plugin + * to help with frontend development without a backend. + */ + +/** + * Generate a random number between min and max + * @param {number} min - Minimum value + * @param {number} max - Maximum value + * @returns {number} Random number + */ +const getRandomNumber = (min, max) => { + return Math.random() * (max - min) + min; +}; + +/** + * Generate a set of mock search results + * @param {string} query - The search query + * @param {number} videoDuration - Duration of the video in seconds + * @param {number} [resultCount=5] - Number of results to generate + * @returns {Array} Array of search result objects + */ +const generateMockResults = (query, videoDuration, resultCount = 5) => { + // Limit the number of results between 1 and 10 + const count = Math.min(Math.max(1, resultCount), 10); + + // Create an array of search results + const results = []; + + for (let i = 0; i < count; i++) { + // Generate a random start time between 0 and (duration - 10 seconds) + const startTime = getRandomNumber(0, Math.max(0, videoDuration - 10)); + + // Generate a random segment length between 2 and 10 seconds + const segmentLength = getRandomNumber(2, Math.min(10, videoDuration - startTime)); + + // End time is start time plus segment length + const endTime = startTime + segmentLength; + + // Create a mock result object + results.push({ + id: `result-${i + 1}`, + startTime: parseFloat(startTime.toFixed(2)), + endTime: parseFloat(endTime.toFixed(2)), + confidence: parseFloat(getRandomNumber(0.6, 0.98).toFixed(2)), + text: `"${query}" match at ${Math.floor(startTime / 60)}:${Math.floor(startTime % 60).toString().padStart(2, '0')}` + }); + } + + // Sort results by start time + return results.sort((a, b) => a.startTime - b.startTime); +}; + +export { generateMockResults }; diff --git a/src/plugins/visual-search/visual-search.js b/src/plugins/visual-search/visual-search.js new file mode 100644 index 000000000..f85ae8f96 --- /dev/null +++ b/src/plugins/visual-search/visual-search.js @@ -0,0 +1,119 @@ +import videojs from 'video.js'; +import './visual-search.scss'; +import { SearchButton } from './components/SearchButton'; +import { SearchInput } from './components/SearchInput'; +import { SearchResults } from './components/SearchResults'; +import { generateMockResults } from './utils/mockData'; + +const visualSearch = (options, player) => { + player.addClass('vjs-visual-search'); + + let isSearchActive = false; + + const searchResults = SearchResults(player); + + const performSearch = async query => { + try { + let results; + + if (options.useMockData) { + results = generateMockResults(query, player.duration(), options.mockResultCount || 5); + searchResults.displayResults(results); + } else { + + // ToDo: construct these search URLs using getCloudinaryUrl + // https://res.cloudinary.com/demo/video/upload/fl_getinfo:search_text_woman/marketing-video-2025 + // Or fl_getingo:search_b64_ + + const url = 'search endpoint'; + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`Search request failed with status: ${response.status}`); + } + + results = await response.json(); + searchResults.displayResults(results); + + } + + if (results && !player.hasStarted()) { + // Make sure the progress bar is visible + player.play().then(() => player.pause()); + } + } catch (error) { + console.error('Error performing visual search:', error); + } + }; + + const clearUI = () => { + searchResults.clearMarkers(); + player.$('.vjs-visual-search-wrapper')?.remove(); + }; + + const createSearchUI = () => { + clearUI(); + + const titleBar = player.$('.vjs-title-bar'); + if (titleBar) { + titleBar.classList.remove('vjs-hidden'); + } + + const searchContainer = videojs.dom.createEl('div', { + className: 'vjs-visual-search-wrapper' + }); + + // Handle the search icon click (expand or submit) + const handleSearchButtonClick = () => { + if (!isSearchActive) { + isSearchActive = true; + searchContainer.classList.add('vjs-visual-search-active'); + searchInput.input.focus(); + } else { + const query = searchInput.input.value.trim(); + if (query) { + performSearch(query); + } + } + }; + + const closeSearch = () => { + if (isSearchActive) { + isSearchActive = false; + searchContainer.classList.remove('vjs-visual-search-active'); + searchInput.input.value = ''; + + searchResults.clearMarkers(); + } + }; + + const searchButton = SearchButton(handleSearchButtonClick); + const searchInput = SearchInput(performSearch, closeSearch); + searchContainer.appendChild(searchButton); + searchContainer.appendChild(searchInput.element); + + titleBar.prepend(searchContainer); + + player.on('keydown', e => { + if (e.key === 'Escape' && isSearchActive) { + closeSearch(); + } + }); + }; + + createSearchUI(); + + // Public methods + player.visualSearch = { + createSearchUI, + clearUI + }; +}; + +export default visualSearch; diff --git a/src/plugins/visual-search/visual-search.scss b/src/plugins/visual-search/visual-search.scss new file mode 100644 index 000000000..f699ad075 --- /dev/null +++ b/src/plugins/visual-search/visual-search.scss @@ -0,0 +1,145 @@ +.vjs-visual-search { + .vjs-visual-search-wrapper { + position: absolute; + top: 1.5em; + right: 1.5em; + margin-left: auto; + display: flex; + align-items: center; + transition: all 0.3s ease; + border-radius: 1.5em; + overflow: hidden; + background-color: transparent; + pointer-events: auto; + + &.vjs-visual-search-active { + background-color: color-mix(in srgb, var(--color-base) 60%, transparent); + backdrop-filter: blur(10px); + + .vjs-visual-search-form { + width: 13.75em; + margin-right: 0.25em; + opacity: 1; + } + + .vjs-visual-search-button { + background-color: transparent; + text-shadow: none; + + &:hover { + text-shadow: 0 0 0.5em var(--color-accent); + } + } + } + + &:hover:not(.vjs-visual-search-active) { + .vjs-visual-search-button { + background-color: color-mix(in srgb, var(--color-base) 25%, transparent); + } + } + } + + .vjs-visual-search-button { + background: transparent; + border: none; + color: var(--color-text); + cursor: pointer; + width: 2.75em; + height: 2.75em; + padding: 0.1em; + opacity: 0.9; + border-radius: 50%; + transition: all 0.25s ease; + z-index: 2; + flex-shrink: 0; + text-shadow: + 0 0 1em var(--color-base); + + &:hover { + opacity: 1; + } + + .vjs-icon-search:before { + font-size: 1.8em; + } + } + + .vjs-visual-search-form { + display: flex; + align-items: center; + width: 0; + opacity: 0; + transition: all 0.3s ease; + overflow: hidden; + } + + .vjs-visual-search-input { + background: transparent; + border: none; + color: var(--color-text); + font-size: 0.938em; + padding: 0; + width: 100%; + height: 2.25em; + outline: none; + + &::placeholder { + color: color-mix(in srgb, white 70%, transparent); + } + } + + .vjs-visual-search-close { + background: transparent; + border: none; + color: var(--color-text); + cursor: pointer; + width: 2em; + height: 2em; + padding: 0.25em; + opacity: 0.7; + transition: opacity 0.2s ease; + flex-shrink: 0; + text-shadow: 0 0 0.25em var(--color-base); + + &:hover { + opacity: 1; + } + + .vjs-icon-close:before { + font-size: 1em; + } + } + + // Search result markers in progress bar + .vjs-visual-search-marker { + position: absolute; + height: 100%; + background-color: color-mix(in srgb, var(--color-text) 60%, transparent); + pointer-events: auto; + cursor: pointer; + z-index: 1; + transition: background-color 0.2s ease; + background-image: linear-gradient( + 45deg, + color-mix(in srgb, var(--color-accent) 80%, transparent) 25%, + transparent 25%, + transparent 50%, + color-mix(in srgb, var(--color-accent) 80%, transparent) 50%, + color-mix(in srgb, var(--color-accent) 80%, transparent) 75%, + transparent 75%, + transparent + ); + background-size: 0.625em 0.625em; + + &:hover { + background-color: color-mix(in srgb, gold 90%, transparent); + } + } +} + +// Expand progress bar when search results are active +.vjs-visual-search-results-active { + .vjs-progress-control { + height: 0.75em !important; + } +} \ No newline at end of file diff --git a/src/utils/get-analytics-player-options.js b/src/utils/get-analytics-player-options.js index 9a0b9bdab..830bc676e 100644 --- a/src/utils/get-analytics-player-options.js +++ b/src/utils/get-analytics-player-options.js @@ -37,6 +37,7 @@ const getTranscriptOptions = (textTracks = {}) => { const getSourceOptions = (sourceOptions = {}) => ({ chapters: sourceOptions.chapters && (sourceOptions.chapters.url ? 'url' : 'inline-chapters'), + visualSearch: hasConfig(sourceOptions.visualSearch), recommendations: sourceOptions.recommendations && sourceOptions.recommendations.length, shoppable: hasConfig(sourceOptions.shoppable), shoppableProductsLength: sourceOptions.shoppable && sourceOptions.shoppable.products && sourceOptions.shoppable.products.length, diff --git a/src/validators/validators.js b/src/validators/validators.js index d0d096b56..fa16c4339 100644 --- a/src/validators/validators.js +++ b/src/validators/validators.js @@ -86,6 +86,7 @@ export const sourceValidators = { raw_transformation: validator.isString, shoppable: validator.isPlainObject, chapters: validator.or(validator.isBoolean, validator.isPlainObject), + visualSearch: validator.or(validator.isBoolean, validator.isPlainObject), interactionAreas: { enable: validator.isBoolean, template: validator.or(validator.isString(INTERACTION_AREAS_TEMPLATE), validator.isArray), diff --git a/src/video-player.const.js b/src/video-player.const.js index 31905a627..2198dde92 100644 --- a/src/video-player.const.js +++ b/src/video-player.const.js @@ -38,7 +38,8 @@ export const PLAYER_PARAMS = CLOUDINARY_PARAMS.concat([ 'aiHighlightsGraph', 'chapters', 'queryParams', - 'type' + 'type', + 'visualSearch' ]); export const CLOUDINARY_CONFIG_PARAM = [ diff --git a/src/video-player.js b/src/video-player.js index f49f7214c..a951e4c64 100644 --- a/src/video-player.js +++ b/src/video-player.js @@ -198,6 +198,7 @@ class VideoPlayer extends Utils.mixin(Eventable) { this._initAnalytics(); this._initCloudinaryAnalytics(); this._initFloatingPlayer(); + this._initVisualSearch(); this._initColors(); this._initTextTracks(); this._initHighlightsGraph(); @@ -343,6 +344,19 @@ class VideoPlayer extends Utils.mixin(Eventable) { }); } + _initVisualSearch() { + // Listen for source changes to apply visual search based on source config + this.videojs.on(PLAYER_EVENT.CLD_SOURCE_CHANGED, (e, { source }) => { + if (source._visualSearch && this.videojs.visualSearch) { + isFunction(this.videojs.visualSearch) + ? this.videojs.visualSearch(source._visualSearch) + : this.videojs.visualSearch.createSearchUI(source._visualSearch); + } else if (!source._visualSearch && this.videojs.visualSearch?.clearUI) { + this.videojs.visualSearch.clearUI(); + } + }); + } + _initColors () { this.videojs.colors(this.playerOptions.colors ? { colors: this.playerOptions.colors } : {}); } From 9501d51ffb89e59542f31ebfe4c5755e74f8e91c Mon Sep 17 00:00:00 2001 From: Tsachi Shlidor Date: Tue, 1 Apr 2025 17:21:02 +0300 Subject: [PATCH 02/12] feat: visual search plugin --- src/plugins/visual-search/visual-search.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/visual-search/visual-search.js b/src/plugins/visual-search/visual-search.js index f85ae8f96..7d27b8d98 100644 --- a/src/plugins/visual-search/visual-search.js +++ b/src/plugins/visual-search/visual-search.js @@ -53,6 +53,7 @@ const visualSearch = (options, player) => { }; const clearUI = () => { + isSearchActive = false; searchResults.clearMarkers(); player.$('.vjs-visual-search-wrapper')?.remove(); }; From 3127b5fc2ed3562dbf1fe14b3204e7a3b093bc59 Mon Sep 17 00:00:00 2001 From: Tsachi Shlidor Date: Wed, 2 Apr 2025 11:55:57 +0300 Subject: [PATCH 03/12] feat: visual search plugin --- .../visual-search/components/SearchResults.js | 7 ++-- src/plugins/visual-search/utils/mockData.js | 9 ++-- src/plugins/visual-search/visual-search.js | 21 ++++++---- src/plugins/visual-search/visual-search.scss | 42 ++++++++++--------- 4 files changed, 41 insertions(+), 38 deletions(-) diff --git a/src/plugins/visual-search/components/SearchResults.js b/src/plugins/visual-search/components/SearchResults.js index 347ee4664..a2a637b08 100644 --- a/src/plugins/visual-search/components/SearchResults.js +++ b/src/plugins/visual-search/components/SearchResults.js @@ -17,18 +17,17 @@ export const SearchResults = (player) => { // Add markers for each result results.forEach(result => { - const { startTime, endTime } = result; + const { start_time, end_time } = result; const markerEl = videojs.dom.createEl('div', { className: 'vjs-visual-search-marker', - style: `left: ${(startTime / total) * 100}%; width: ${((endTime - startTime) / total) * 100}%` + style: `left: ${(start_time / total) * 100}%; width: ${((end_time - start_time) / total) * 100}%` }); seekBar.el().appendChild(markerEl); // Add click handler to jump to this time markerEl.addEventListener('click', () => { - player.currentTime(startTime); - player.play(); + player.currentTime(start_time); }); }); diff --git a/src/plugins/visual-search/utils/mockData.js b/src/plugins/visual-search/utils/mockData.js index 6dd9f2616..f2a0c25c5 100644 --- a/src/plugins/visual-search/utils/mockData.js +++ b/src/plugins/visual-search/utils/mockData.js @@ -39,16 +39,13 @@ const generateMockResults = (query, videoDuration, resultCount = 5) => { // Create a mock result object results.push({ - id: `result-${i + 1}`, - startTime: parseFloat(startTime.toFixed(2)), - endTime: parseFloat(endTime.toFixed(2)), - confidence: parseFloat(getRandomNumber(0.6, 0.98).toFixed(2)), - text: `"${query}" match at ${Math.floor(startTime / 60)}:${Math.floor(startTime % 60).toString().padStart(2, '0')}` + start_time: parseFloat(startTime.toFixed(2)), + end_time: parseFloat(endTime.toFixed(2)) }); } // Sort results by start time - return results.sort((a, b) => a.startTime - b.startTime); + return results.sort((a, b) => a.start_time - b.end_time); }; export { generateMockResults }; diff --git a/src/plugins/visual-search/visual-search.js b/src/plugins/visual-search/visual-search.js index 7d27b8d98..a9e79d195 100644 --- a/src/plugins/visual-search/visual-search.js +++ b/src/plugins/visual-search/visual-search.js @@ -1,10 +1,11 @@ import videojs from 'video.js'; -import './visual-search.scss'; import { SearchButton } from './components/SearchButton'; import { SearchInput } from './components/SearchInput'; import { SearchResults } from './components/SearchResults'; import { generateMockResults } from './utils/mockData'; +import './visual-search.scss'; + const visualSearch = (options, player) => { player.addClass('vjs-visual-search'); @@ -15,19 +16,22 @@ const visualSearch = (options, player) => { const performSearch = async query => { try { let results; - if (options.useMockData) { results = generateMockResults(query, player.duration(), options.mockResultCount || 5); searchResults.displayResults(results); } else { - // ToDo: construct these search URLs using getCloudinaryUrl - // https://res.cloudinary.com/demo/video/upload/fl_getinfo:search_text_woman/marketing-video-2025 - // Or fl_getingo:search_b64_ + const source = player.cloudinary.source(); + const publicId = source.publicId(); + const transformation = Object.assign({}, source.transformation()); - const url = 'search endpoint'; + transformation.flags = transformation.flags || []; + transformation.flags.push(`getinfo:search_text_${query}`); - const response = await fetch(url, { + const visualSearchSrc = source.config() + .url(`${publicId}`, { transformation }); + + const response = await fetch(visualSearchSrc, { method: 'GET', headers: { 'Content-Type': 'application/json' @@ -39,8 +43,7 @@ const visualSearch = (options, player) => { } results = await response.json(); - searchResults.displayResults(results); - + searchResults.displayResults(results.timestamps); } if (results && !player.hasStarted()) { diff --git a/src/plugins/visual-search/visual-search.scss b/src/plugins/visual-search/visual-search.scss index f699ad075..839b6ec1d 100644 --- a/src/plugins/visual-search/visual-search.scss +++ b/src/plugins/visual-search/visual-search.scss @@ -15,28 +15,32 @@ &.vjs-visual-search-active { background-color: color-mix(in srgb, var(--color-base) 60%, transparent); backdrop-filter: blur(10px); - + .vjs-visual-search-form { width: 13.75em; margin-right: 0.25em; opacity: 1; } - + .vjs-visual-search-button { background-color: transparent; text-shadow: none; - + &:hover { text-shadow: 0 0 0.5em var(--color-accent); } } } - + &:hover:not(.vjs-visual-search-active) { .vjs-visual-search-button { background-color: color-mix(in srgb, var(--color-base) 25%, transparent); } } + + + .vjs-title-bar-title { + padding-right: 2em; + } } .vjs-visual-search-button { @@ -52,8 +56,7 @@ transition: all 0.25s ease; z-index: 2; flex-shrink: 0; - text-shadow: - 0 0 1em var(--color-base); + text-shadow: 0 0 1em var(--color-base); &:hover { opacity: 1; @@ -82,12 +85,12 @@ width: 100%; height: 2.25em; outline: none; - + &::placeholder { color: color-mix(in srgb, white 70%, transparent); } } - + .vjs-visual-search-close { background: transparent; border: none; @@ -100,39 +103,40 @@ transition: opacity 0.2s ease; flex-shrink: 0; text-shadow: 0 0 0.25em var(--color-base); - + &:hover { opacity: 1; } - + .vjs-icon-close:before { font-size: 1em; } } // Search result markers in progress bar - .vjs-visual-search-marker { + .vjs-visual-search-marker { position: absolute; height: 100%; - background-color: color-mix(in srgb, var(--color-text) 60%, transparent); - pointer-events: auto; - cursor: pointer; z-index: 1; + pointer-events: auto; transition: background-color 0.2s ease; + + $color1: color-mix(in srgb, var(--color-base) 80%, transparent); + $color2: color-mix(in srgb, var(--color-text) 50%, var(--color-accent)); background-image: linear-gradient( 45deg, - color-mix(in srgb, var(--color-accent) 80%, transparent) 25%, + $color1 25%, transparent 25%, transparent 50%, - color-mix(in srgb, var(--color-accent) 80%, transparent) 50%, - color-mix(in srgb, var(--color-accent) 80%, transparent) 75%, + $color1 50%, + $color1 75%, transparent 75%, transparent ); background-size: 0.625em 0.625em; &:hover { - background-color: color-mix(in srgb, gold 90%, transparent); + background-color: color-mix(in srgb, var(--color-text) 50%, var(--color-accent)); } } } @@ -142,4 +146,4 @@ .vjs-progress-control { height: 0.75em !important; } -} \ No newline at end of file +} From 6f81fe0516383a9002108ea01093ff7f050c09f6 Mon Sep 17 00:00:00 2001 From: Tsachi Shlidor Date: Wed, 2 Apr 2025 12:57:07 +0300 Subject: [PATCH 04/12] chore: visual-search example --- docs/es-modules/visual-search.html | 21 ++- docs/visual-search.html | 235 ++++++++++++++++------------- 2 files changed, 143 insertions(+), 113 deletions(-) diff --git a/docs/es-modules/visual-search.html b/docs/es-modules/visual-search.html index 2a34e4490..73971698a 100644 --- a/docs/es-modules/visual-search.html +++ b/docs/es-modules/visual-search.html @@ -1,4 +1,4 @@ - + @@ -27,7 +27,7 @@

    Visual Search

    muted playsinline > - +

    Full documentationVisual Search diff --git a/docs/visual-search.html b/docs/visual-search.html index e21b4858c..0084fd9a8 100644 --- a/docs/visual-search.html +++ b/docs/visual-search.html @@ -1,23 +1,33 @@ - + - - - - Cloudinary Video Player - Visual Search - - - - - - - - - - - + + + + + + + + - - - - - - -

    - -

    Cloudinary Video Player

    -

    Visual Search Plugin

    - - - -

    Playlist with mixed content

    - - - -

    - Full documentation -

    - -

    Example Code:

    -
    +    
    +
    +    
    +  
    +
    +  
    +    
    + +

    Cloudinary Video Player

    +

    Visual Search Plugin

    + + + +
    + +

    Playlist with mixed content

    + + + +

    + Full documentation +

    + +

    Example Code:

    +
           
    -<video
    -  id="player"
    -  playsinline
    -  controls
    -  class="cld-video-player cld-video-player-skin-dark"
    -  width="500"
    -></video>
    +    <video
    +      id="player"
    +      playsinline
    +      controls
    +      class="cld-video-player cld-fluid"
    +      width="500"
    +    ></video>
           
           
    -// Initialize player with visual search plugin
    -const player = cloudinary.videoPlayer('player', {
    -  cloudName: 'demo',
    -  publicId: 'elephants',
    -  visualSearch: true
    -});
    +    // Initialize player with visual search plugin
    +    const player = cloudinary.videoPlayer('player', {
    +      cloudName: 'demo',
    +      publicId: 'elephants',
    +      visualSearch: true
    +    });
     
           
         
    -
    - - - - \ No newline at end of file +
    + + From 3d0d8898f4bea8dc0710d5eb72b2df1aaeff6a49 Mon Sep 17 00:00:00 2001 From: Tsachi Shlidor Date: Wed, 2 Apr 2025 16:08:29 +0300 Subject: [PATCH 05/12] feat: visual search plugin --- .../visual-search/components/SearchButton.js | 10 +++++--- src/plugins/visual-search/visual-search.js | 6 ++++- src/plugins/visual-search/visual-search.scss | 25 ++++++++++++++++++- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/plugins/visual-search/components/SearchButton.js b/src/plugins/visual-search/components/SearchButton.js index eb016fdbe..0e1e39721 100644 --- a/src/plugins/visual-search/components/SearchButton.js +++ b/src/plugins/visual-search/components/SearchButton.js @@ -6,11 +6,15 @@ export const SearchButton = (onClick) => { title: 'Search video content' }); - // Add search icon - const iconElement = videojs.dom.createEl('span', { + const searchIcon = videojs.dom.createEl('span', { className: 'vjs-icon-search' }); - button.appendChild(iconElement); + button.appendChild(searchIcon); + + const spinnerIcon = videojs.dom.createEl('span', { + className: 'vjs-icon-spinner vjs-visual-search-spinner' + }); + button.appendChild(spinnerIcon); button.addEventListener('click', onClick); diff --git a/src/plugins/visual-search/visual-search.js b/src/plugins/visual-search/visual-search.js index a9e79d195..19c7a3c5b 100644 --- a/src/plugins/visual-search/visual-search.js +++ b/src/plugins/visual-search/visual-search.js @@ -14,13 +14,15 @@ const visualSearch = (options, player) => { const searchResults = SearchResults(player); const performSearch = async query => { + const searchButton = player.$('.vjs-visual-search-button'); + searchButton.classList.add('vjs-visual-search-loading'); + try { let results; if (options.useMockData) { results = generateMockResults(query, player.duration(), options.mockResultCount || 5); searchResults.displayResults(results); } else { - const source = player.cloudinary.source(); const publicId = source.publicId(); const transformation = Object.assign({}, source.transformation()); @@ -52,6 +54,8 @@ const visualSearch = (options, player) => { } } catch (error) { console.error('Error performing visual search:', error); + } finally { + searchButton.classList.remove('vjs-visual-search-loading'); } }; diff --git a/src/plugins/visual-search/visual-search.scss b/src/plugins/visual-search/visual-search.scss index 839b6ec1d..c7d4ed2ae 100644 --- a/src/plugins/visual-search/visual-search.scss +++ b/src/plugins/visual-search/visual-search.scss @@ -62,9 +62,23 @@ opacity: 1; } - .vjs-icon-search:before { + > span:before { font-size: 1.8em; } + + .vjs-visual-search-spinner { + display: none; + animation: vjs-visual-search-spin 1s linear infinite; + } + + &.vjs-visual-search-loading { + .vjs-icon-search { + display: none; + } + .vjs-visual-search-spinner { + display: block; + } + } } .vjs-visual-search-form { @@ -147,3 +161,12 @@ height: 0.75em !important; } } + +@keyframes vjs-visual-search-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} From ad1a7e80dcac738dbef10813574e89bcc9afd9dd Mon Sep 17 00:00:00 2001 From: Tsachi Shlidor Date: Wed, 2 Apr 2025 16:23:14 +0300 Subject: [PATCH 06/12] feat: visual search plugin --- src/plugins/visual-search/visual-search.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/plugins/visual-search/visual-search.scss b/src/plugins/visual-search/visual-search.scss index c7d4ed2ae..7c34cebff 100644 --- a/src/plugins/visual-search/visual-search.scss +++ b/src/plugins/visual-search/visual-search.scss @@ -160,6 +160,10 @@ .vjs-progress-control { height: 0.75em !important; } + .vjs-vtt-thumbnail-display, + .vjs-chapter-display { + translate: 0 -0.75em; + } } @keyframes vjs-visual-search-spin { From 2f67ef8cba57b8d6c41789b1ec10d1506026f8a5 Mon Sep 17 00:00:00 2001 From: Tsachi Shlidor Date: Thu, 3 Apr 2025 12:24:01 +0300 Subject: [PATCH 07/12] feat: visual search plugin --- .../visual-search/components/SearchResults.js | 11 ++++- src/plugins/visual-search/visual-search.scss | 47 +++++++++---------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/plugins/visual-search/components/SearchResults.js b/src/plugins/visual-search/components/SearchResults.js index a2a637b08..6e9396cd1 100644 --- a/src/plugins/visual-search/components/SearchResults.js +++ b/src/plugins/visual-search/components/SearchResults.js @@ -3,6 +3,7 @@ import videojs from 'video.js'; export const SearchResults = (player) => { const clearMarkers = () => { player.$$('.vjs-visual-search-marker').forEach(el => el.remove()); + player.$$('.vjs-visual-search-results-wrapper').forEach(el => el.remove()); // Remove the class that indicates search results are displayed player.removeClass('vjs-visual-search-results-active'); @@ -15,6 +16,11 @@ export const SearchResults = (player) => { const total = player.duration(); const seekBar = player.controlBar.progressControl.seekBar; + // Create wrapper for search results + const wrapperEl = videojs.dom.createEl('div', { + className: 'vjs-visual-search-results-wrapper' + }); + // Add markers for each result results.forEach(result => { const { start_time, end_time } = result; @@ -23,7 +29,7 @@ export const SearchResults = (player) => { style: `left: ${(start_time / total) * 100}%; width: ${((end_time - start_time) / total) * 100}%` }); - seekBar.el().appendChild(markerEl); + wrapperEl.appendChild(markerEl); // Add click handler to jump to this time markerEl.addEventListener('click', () => { @@ -31,6 +37,9 @@ export const SearchResults = (player) => { }); }); + // Add wrapper to seek bar + seekBar.el().appendChild(wrapperEl); + // Add a class to indicate search results are displayed if (results.length > 0) { player.addClass('vjs-visual-search-results-active'); diff --git a/src/plugins/visual-search/visual-search.scss b/src/plugins/visual-search/visual-search.scss index 7c34cebff..c4234a418 100644 --- a/src/plugins/visual-search/visual-search.scss +++ b/src/plugins/visual-search/visual-search.scss @@ -127,41 +127,38 @@ } } - // Search result markers in progress bar - .vjs-visual-search-marker { + .vjs-visual-search-results-wrapper { position: absolute; - height: 100%; + bottom: 100%; + left: 0; + width: 100%; + height: 0.75em; z-index: 1; + $bg1: color-mix(in srgb, var(--color-accent) 15%, transparent); + $bg2: color-mix(in srgb, var(--color-base) 20%, transparent); + background: + linear-gradient(to right, $bg1, $bg1), + linear-gradient(to right, $bg2, $bg2); + } + + .vjs-visual-search-marker { + position: absolute; + height: 100%; + background-color: var(--color-accent); + opacity: 0.7; pointer-events: auto; - transition: background-color 0.2s ease; - - $color1: color-mix(in srgb, var(--color-base) 80%, transparent); - $color2: color-mix(in srgb, var(--color-text) 50%, var(--color-accent)); - background-image: linear-gradient( - 45deg, - $color1 25%, - transparent 25%, - transparent 50%, - $color1 50%, - $color1 75%, - transparent 75%, - transparent - ); - background-size: 0.625em 0.625em; + cursor: pointer; + transition: opacity 0.2s ease; &:hover { - background-color: color-mix(in srgb, var(--color-text) 50%, var(--color-accent)); + opacity: 0.8; } } } -// Expand progress bar when search results are active +// Push seek-thumbs/chapters/time up when search results are active .vjs-visual-search-results-active { - .vjs-progress-control { - height: 0.75em !important; - } - .vjs-vtt-thumbnail-display, - .vjs-chapter-display { + .vjs-mouse-display { translate: 0 -0.75em; } } From 98456323a95ee3e1d0676b9990cd9d69f35a0e8f Mon Sep 17 00:00:00 2001 From: Tsachi Shlidor Date: Thu, 3 Apr 2025 16:15:44 +0300 Subject: [PATCH 08/12] feat: visual search plugin --- docs/visual-search.html | 27 ++++++----- src/plugins/chapters/chapters.js | 2 +- src/plugins/visual-search/utils/mockData.js | 51 --------------------- src/plugins/visual-search/visual-search.js | 40 +++++++--------- src/validators/validators.js | 2 +- src/video-player.js | 2 + 6 files changed, 35 insertions(+), 89 deletions(-) delete mode 100644 src/plugins/visual-search/utils/mockData.js diff --git a/docs/visual-search.html b/docs/visual-search.html index 0084fd9a8..175f04195 100644 --- a/docs/visual-search.html +++ b/docs/visual-search.html @@ -50,28 +50,30 @@ player.source({ publicId: 'marketing-video-2025', info: { title: 'Visual search', subtitle: 'Real results from indexed video' }, - chapters: { - 0: 'Opening credits', - 3: 'A dangerous quest', - 8: 'The attack' - }, visualSearch: true }); // Playlist with mixed content var source1 = { - publicId: 'marmots', - info: { title: 'Visual search mocked', subtitle: 'Random results from mocked data' }, - visualSearch: { - useMockData: true - } + publicId: 'marketing-video-2025', + info: { title: 'Visual search', subtitle: 'Real results from indexed video' }, + visualSearch: true, }; var source2 = { - publicId: 'marketing-video-2025', + publicId: 'docs/ping_pong_paddle', + info: { title: 'Visual search with chapters', subtitle: 'Real results from indexed video' }, + visualSearch: true, + chapters: { + 0: 'Opening credits', + 3: 'A dangerous quest', + 8: 'The attack' + }, + }; + var source3 = { + publicId: 'docs/pizza_slice', info: { title: 'Visual search', subtitle: 'Real results from indexed video' }, visualSearch: true }; - var source3 = { publicId: 'elephants', info: { title: 'Visual search disabled' } }; var source4 = { publicId: 'snow_horses', info: { title: 'Visual search disabled' } }; var playlistSources = [source1, source2, source3, source4]; @@ -83,6 +85,7 @@ var playerPlaylist = cloudinary.videoPlayer('player-playlist', { cloudName: 'demo', + aspectRatio: '16:9', playlistWidget: { direction: 'horizontal', total: 4 diff --git a/src/plugins/chapters/chapters.js b/src/plugins/chapters/chapters.js index 4b4c102a4..418d53772 100644 --- a/src/plugins/chapters/chapters.js +++ b/src/plugins/chapters/chapters.js @@ -193,7 +193,7 @@ const ChaptersPlugin = (function () { const getChapterFromPoint = point => { const total = this.player.duration(); const seekBarTime = point * total; - const chapter = Array.from(this.chaptersTrack.cues).find(marker => { + const chapter = Array.from(this.chaptersTrack?.cues || []).find(marker => { return seekBarTime >= marker.startTime && seekBarTime <= marker.endTime; }); return chapter ? chapter.text : ''; diff --git a/src/plugins/visual-search/utils/mockData.js b/src/plugins/visual-search/utils/mockData.js deleted file mode 100644 index f2a0c25c5..000000000 --- a/src/plugins/visual-search/utils/mockData.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Generates mock search results for the visual search plugin - * to help with frontend development without a backend. - */ - -/** - * Generate a random number between min and max - * @param {number} min - Minimum value - * @param {number} max - Maximum value - * @returns {number} Random number - */ -const getRandomNumber = (min, max) => { - return Math.random() * (max - min) + min; -}; - -/** - * Generate a set of mock search results - * @param {string} query - The search query - * @param {number} videoDuration - Duration of the video in seconds - * @param {number} [resultCount=5] - Number of results to generate - * @returns {Array} Array of search result objects - */ -const generateMockResults = (query, videoDuration, resultCount = 5) => { - // Limit the number of results between 1 and 10 - const count = Math.min(Math.max(1, resultCount), 10); - - // Create an array of search results - const results = []; - - for (let i = 0; i < count; i++) { - // Generate a random start time between 0 and (duration - 10 seconds) - const startTime = getRandomNumber(0, Math.max(0, videoDuration - 10)); - - // Generate a random segment length between 2 and 10 seconds - const segmentLength = getRandomNumber(2, Math.min(10, videoDuration - startTime)); - - // End time is start time plus segment length - const endTime = startTime + segmentLength; - - // Create a mock result object - results.push({ - start_time: parseFloat(startTime.toFixed(2)), - end_time: parseFloat(endTime.toFixed(2)) - }); - } - - // Sort results by start time - return results.sort((a, b) => a.start_time - b.end_time); -}; - -export { generateMockResults }; diff --git a/src/plugins/visual-search/visual-search.js b/src/plugins/visual-search/visual-search.js index 19c7a3c5b..c215d019d 100644 --- a/src/plugins/visual-search/visual-search.js +++ b/src/plugins/visual-search/visual-search.js @@ -2,7 +2,6 @@ import videojs from 'video.js'; import { SearchButton } from './components/SearchButton'; import { SearchInput } from './components/SearchInput'; import { SearchResults } from './components/SearchResults'; -import { generateMockResults } from './utils/mockData'; import './visual-search.scss'; @@ -18,36 +17,29 @@ const visualSearch = (options, player) => { searchButton.classList.add('vjs-visual-search-loading'); try { - let results; - if (options.useMockData) { - results = generateMockResults(query, player.duration(), options.mockResultCount || 5); - searchResults.displayResults(results); - } else { - const source = player.cloudinary.source(); - const publicId = source.publicId(); - const transformation = Object.assign({}, source.transformation()); - - transformation.flags = transformation.flags || []; - transformation.flags.push(`getinfo:search_text_${query}`); + const source = player.cloudinary.source(); + const publicId = source.publicId(); + const transformation = Object.assign({}, source.transformation()); - const visualSearchSrc = source.config() - .url(`${publicId}`, { transformation }); + transformation.flags = transformation.flags || []; + transformation.flags.push(`getinfo:search_b64_${btoa(query)}`); - const response = await fetch(visualSearchSrc, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }); + const visualSearchSrc = source.config().url(`${publicId}`, { transformation }); - if (!response.ok) { - throw new Error(`Search request failed with status: ${response.status}`); + const response = await fetch(visualSearchSrc, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' } + }); - results = await response.json(); - searchResults.displayResults(results.timestamps); + if (!response.ok) { + throw new Error(`Search request failed with status: ${response.status}`); } + const results = await response.json(); + searchResults.displayResults(results.timestamps); + if (results && !player.hasStarted()) { // Make sure the progress bar is visible player.play().then(() => player.pause()); diff --git a/src/validators/validators.js b/src/validators/validators.js index fa16c4339..764bbe320 100644 --- a/src/validators/validators.js +++ b/src/validators/validators.js @@ -86,7 +86,7 @@ export const sourceValidators = { raw_transformation: validator.isString, shoppable: validator.isPlainObject, chapters: validator.or(validator.isBoolean, validator.isPlainObject), - visualSearch: validator.or(validator.isBoolean, validator.isPlainObject), + visualSearch: validator.or(validator.isBoolean), interactionAreas: { enable: validator.isBoolean, template: validator.or(validator.isString(INTERACTION_AREAS_TEMPLATE), validator.isArray), diff --git a/src/video-player.js b/src/video-player.js index a951e4c64..3386efbee 100644 --- a/src/video-player.js +++ b/src/video-player.js @@ -332,6 +332,8 @@ class VideoPlayer extends Utils.mixin(Eventable) { isFunction(this.videojs.chapters) ? this.videojs.chapters(source._chapters) : this.videojs.chapters.src(source._chapters); + } else if (this.videojs.chapters?.resetPlugin) { + this.videojs.chapters.resetPlugin(); } }); } From 7d5a057008cb39a769524d36845c4d7839ea372c Mon Sep 17 00:00:00 2001 From: Tsachi Shlidor Date: Thu, 3 Apr 2025 16:17:04 +0300 Subject: [PATCH 09/12] chore: visual-search e2e --- .../e2e/specs/NonESM/linksConsolErros.spec.ts | 2 +- .../e2e/specs/NonESM/visualSearchPage.spec.ts | 36 +++++++++++++++++++ test/e2e/src/pom/PageManager.ts | 8 +++++ test/e2e/src/pom/visualSearchPage.ts | 20 +++++++++++ test/e2e/testData/ExampleLinkNames.ts | 1 + test/e2e/testData/pageLinksData.ts | 1 + 6 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 test/e2e/specs/NonESM/visualSearchPage.spec.ts create mode 100644 test/e2e/src/pom/visualSearchPage.ts diff --git a/test/e2e/specs/NonESM/linksConsolErros.spec.ts b/test/e2e/specs/NonESM/linksConsolErros.spec.ts index 98791381c..0d550969c 100644 --- a/test/e2e/specs/NonESM/linksConsolErros.spec.ts +++ b/test/e2e/specs/NonESM/linksConsolErros.spec.ts @@ -22,7 +22,7 @@ for (const link of LINKS) { * Testing number of links in page. */ vpTest('Link count test', async ({ page }) => { - const expectedNumberOfLinks = 36; + const expectedNumberOfLinks = 37; const numberOfLinks = await page.getByRole('link').count(); expect(numberOfLinks).toBe(expectedNumberOfLinks); }); diff --git a/test/e2e/specs/NonESM/visualSearchPage.spec.ts b/test/e2e/specs/NonESM/visualSearchPage.spec.ts new file mode 100644 index 000000000..6589e5c7f --- /dev/null +++ b/test/e2e/specs/NonESM/visualSearchPage.spec.ts @@ -0,0 +1,36 @@ +import { vpTest } from '../../fixtures/vpTest'; +import { test } from '@playwright/test'; +import { waitForPageToLoadWithTimeout } from '../../src/helpers/waitForPageToLoadWithTimeout'; +import { getLinkByName } from '../../testData/pageLinksData'; +import { ExampleLinkName } from '../../testData/ExampleLinkNames'; + +const link = getLinkByName(ExampleLinkName.VisualSearch); + +vpTest(`Test if video on visual search page is playing as expected`, async ({ page, pomPages }) => { + await test.step('Navigate to visual search page by clicking on link', async () => { + await pomPages.mainPage.clickLinkByName(link.name); + await waitForPageToLoadWithTimeout(page, 10000); + }); + + // Wait for both video elements to be ready + await test.step('Wait for video elements to be ready', async () => { + await page.waitForSelector('#player_html5_api', { state: 'visible' }); + await page.waitForSelector('#player-playlist_html5_api', { state: 'visible' }); + }); + + await test.step('Click play button on visual search video', async () => { + await pomPages.visualSearchPage.visualSearchVideoComponent.clickPlay(); + }); + + await test.step('Validating that visual search video is playing', async () => { + await pomPages.visualSearchPage.visualSearchVideoComponent.validateVideoIsPlaying(true); + }); + + await test.step('Click play button on playlist video', async () => { + await pomPages.visualSearchPage.visualSearchPlaylistVideoComponent.clickPlay(); + }); + + await test.step('Validating that visual search playlist video is playing', async () => { + await pomPages.visualSearchPage.visualSearchPlaylistVideoComponent.validateVideoIsPlaying(true); + }); +}); diff --git a/test/e2e/src/pom/PageManager.ts b/test/e2e/src/pom/PageManager.ts index 48afd7052..112e9fdd2 100644 --- a/test/e2e/src/pom/PageManager.ts +++ b/test/e2e/src/pom/PageManager.ts @@ -28,6 +28,7 @@ import { SubtitlesAndCaptionsPage } from './subtitlesAndCaptionsPage'; import { VideoTransformationsPage } from './videoTransformationsPage'; import { VastAndVpaidPage } from './vastAndVpaidPage'; import { Vr360VideosPage } from './vr360VideosPage'; +import { VisualSearchPage } from './visualSearchPage'; /** * Page manager, @@ -191,5 +192,12 @@ export class PageManager { public get vr360VideosPage(): Vr360VideosPage { return this.getPage(Vr360VideosPage); } + + /** + * Returns visual search page object + */ + public get visualSearchPage(): VisualSearchPage { + return this.getPage(VisualSearchPage); + } } export default PageManager; diff --git a/test/e2e/src/pom/visualSearchPage.ts b/test/e2e/src/pom/visualSearchPage.ts new file mode 100644 index 000000000..2bb506434 --- /dev/null +++ b/test/e2e/src/pom/visualSearchPage.ts @@ -0,0 +1,20 @@ +import { Page } from '@playwright/test'; +import { VideoComponent } from '../../components/videoComponent'; +import { BasePage } from './BasePage'; + +const VISUAL_SEARCH_PAGE_VIDEO_SELECTOR = '//*[@id="player_html5_api"]'; +const VISUAL_SEARCH_PLAYLIST_VIDEO_SELECTOR = '//*[@id="player-playlist_html5_api"]'; + +/** + * Video player examples visual search page object + */ +export class VisualSearchPage extends BasePage { + public visualSearchVideoComponent: VideoComponent; + public visualSearchPlaylistVideoComponent: VideoComponent; + + constructor(page: Page) { + super(page); + this.visualSearchVideoComponent = new VideoComponent(page, VISUAL_SEARCH_PAGE_VIDEO_SELECTOR); + this.visualSearchPlaylistVideoComponent = new VideoComponent(page, VISUAL_SEARCH_PLAYLIST_VIDEO_SELECTOR); + } +} diff --git a/test/e2e/testData/ExampleLinkNames.ts b/test/e2e/testData/ExampleLinkNames.ts index 9e3d0439a..742777e77 100644 --- a/test/e2e/testData/ExampleLinkNames.ts +++ b/test/e2e/testData/ExampleLinkNames.ts @@ -38,4 +38,5 @@ export enum ExampleLinkName { ESMImports = 'ESM Imports', AllBuild = '/all build', LightBuild = '/light build', + VisualSearch = 'Visual Search', } diff --git a/test/e2e/testData/pageLinksData.ts b/test/e2e/testData/pageLinksData.ts index c88bc0046..d54e43e8e 100644 --- a/test/e2e/testData/pageLinksData.ts +++ b/test/e2e/testData/pageLinksData.ts @@ -39,6 +39,7 @@ export const LINKS: ExampleLinkType[] = [ { name: ExampleLinkName.VR360Videos, endpoint: '360.html' }, { name: ExampleLinkName.EmbeddedIframePlayer, endpoint: 'embedded-iframe.html' }, { name: ExampleLinkName.ESMImports, endpoint: 'cld-vp-esm-pages.netlify.app' }, + { name: ExampleLinkName.VisualSearch, endpoint: 'visual-search.html' }, ]; /** From 2a330451920490d7f015f22247ddc2b441e91356 Mon Sep 17 00:00:00 2001 From: Tsachi Shlidor Date: Thu, 3 Apr 2025 16:23:37 +0300 Subject: [PATCH 10/12] chore: visual search use default spinner --- .../visual-search/components/SearchButton.js | 2 +- src/plugins/visual-search/visual-search.js | 4 ++-- src/plugins/visual-search/visual-search.scss | 18 ++++++++++++------ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/plugins/visual-search/components/SearchButton.js b/src/plugins/visual-search/components/SearchButton.js index 0e1e39721..3ddaf2790 100644 --- a/src/plugins/visual-search/components/SearchButton.js +++ b/src/plugins/visual-search/components/SearchButton.js @@ -12,7 +12,7 @@ export const SearchButton = (onClick) => { button.appendChild(searchIcon); const spinnerIcon = videojs.dom.createEl('span', { - className: 'vjs-icon-spinner vjs-visual-search-spinner' + className: 'vjs-loading-spinner' }); button.appendChild(spinnerIcon); diff --git a/src/plugins/visual-search/visual-search.js b/src/plugins/visual-search/visual-search.js index c215d019d..b2d310992 100644 --- a/src/plugins/visual-search/visual-search.js +++ b/src/plugins/visual-search/visual-search.js @@ -14,7 +14,7 @@ const visualSearch = (options, player) => { const performSearch = async query => { const searchButton = player.$('.vjs-visual-search-button'); - searchButton.classList.add('vjs-visual-search-loading'); + searchButton.classList.add('vjs-waiting'); try { const source = player.cloudinary.source(); @@ -47,7 +47,7 @@ const visualSearch = (options, player) => { } catch (error) { console.error('Error performing visual search:', error); } finally { - searchButton.classList.remove('vjs-visual-search-loading'); + searchButton.classList.remove('vjs-waiting'); } }; diff --git a/src/plugins/visual-search/visual-search.scss b/src/plugins/visual-search/visual-search.scss index c4234a418..78b0cab05 100644 --- a/src/plugins/visual-search/visual-search.scss +++ b/src/plugins/visual-search/visual-search.scss @@ -66,17 +66,23 @@ font-size: 1.8em; } - .vjs-visual-search-spinner { + .vjs-loading-spinner { display: none; - animation: vjs-visual-search-spin 1s linear infinite; + width: 2.15em; + height: 2.15em; + position: absolute; + top: 0.3em; + left: 0.3em; + border-width: 0.4em; + transform: none; } - &.vjs-visual-search-loading { - .vjs-icon-search { + &.vjs-waiting { + > .vjs-icon-search { display: none; } - .vjs-visual-search-spinner { - display: block; + > .vjs-loading-spinner { + display: flex; } } } From 28625c90f8ed5ae2b6d42612494caf48534e452a Mon Sep 17 00:00:00 2001 From: Tsachi Shlidor Date: Thu, 3 Apr 2025 17:06:24 +0300 Subject: [PATCH 11/12] chore: visual search E2E --- test/e2e/specs/ESM/linksConsoleErrorsEsmPage.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/specs/ESM/linksConsoleErrorsEsmPage.spec.ts b/test/e2e/specs/ESM/linksConsoleErrorsEsmPage.spec.ts index c6bb512f8..70b823f49 100644 --- a/test/e2e/specs/ESM/linksConsoleErrorsEsmPage.spec.ts +++ b/test/e2e/specs/ESM/linksConsoleErrorsEsmPage.spec.ts @@ -25,7 +25,7 @@ for (const link of ESM_LINKS) { */ vpTest('ESM page Link count test', async ({ page }) => { await page.goto(ESM_URL); - const expectedNumberOfLinks = 33; + const expectedNumberOfLinks = 34; const numberOfLinks = await page.getByRole('link').count(); expect(numberOfLinks).toBe(expectedNumberOfLinks); }); From 57b7bf8dfea26736387d3da137fdaa8326adbcb0 Mon Sep 17 00:00:00 2001 From: Tsachi Shlidor Date: Sun, 6 Apr 2025 10:37:18 +0300 Subject: [PATCH 12/12] chore: visual search E2E --- test/e2e/specs/NonESM/visualSearchPage.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/specs/NonESM/visualSearchPage.spec.ts b/test/e2e/specs/NonESM/visualSearchPage.spec.ts index 6589e5c7f..6f3b2f337 100644 --- a/test/e2e/specs/NonESM/visualSearchPage.spec.ts +++ b/test/e2e/specs/NonESM/visualSearchPage.spec.ts @@ -14,8 +14,8 @@ vpTest(`Test if video on visual search page is playing as expected`, async ({ pa // Wait for both video elements to be ready await test.step('Wait for video elements to be ready', async () => { - await page.waitForSelector('#player_html5_api', { state: 'visible' }); - await page.waitForSelector('#player-playlist_html5_api', { state: 'visible' }); + await pomPages.visualSearchPage.visualSearchVideoComponent.locator.scrollIntoViewIfNeeded(); + await pomPages.visualSearchPage.visualSearchPlaylistVideoComponent.locator.scrollIntoViewIfNeeded(); }); await test.step('Click play button on visual search video', async () => {