Project One-Click: Need help with cc4c

Hi just replying to let you know that I did get into the root folder and have navigated to where main.js is - can I edit that through ssh? sorry if this is off-topic... didn't mean for that to happen, just figured this might be helpful - but of course, I can always move this out of the thread if that makes more sense.

Thanks. I don't need this. Docker and Portainer have been installed for quite a while on my UGREEN NAS. There's not a lot of documentation on how UGOS behaves.

Just brought it up because you mentioned permission issues on /volume1/@docker not /volume1/docker

Right... that's how UGOS installs it's own version of Docker for whatever reason. I actually have /@docker and /docker - the latter of which only includes /portainer. It's strange how they implement things.

Right, so @docker is for the UGREEN Docker engine and /volume1/docker/ is where you should map container volumes to so you can access them. Like /volume1/docker/portainer/

Yes, but you'll need an editor. Is nano installed, or can you add it?

A @chDVRuser mentioned, WinSCP could be a really nice option too, using the SCP protocol. This assumes you're accessing your NAS from a Windows machine though.

I actually finally just managed to download the main.js file via SSH :slight_smile: just took a while to get there, but you both helped me get to it. Now I'm going to edit and see if I can transfer it back over to see about these edits. Thanks for the work you've done with cc4c!

Still think you need to map your containers into /volume1/docker/

Here is what winscp looks like viewing my /volume1/docker/ directory

1 Like

Ah yes...I installed that and can see my root files - that's much easier to work with than ssh commands. Also, I do think I have a different issue entirely (and I don't think it's been mentioned in the forums for cc4c) where Peacock channels don't like the flavor of Chrome that's running in the container... even though they say they support version 112+ in their documentation, it won't play...and I presume it's because this is linux/ubuntu... so that might have been a moot point for everything!

OK, so @radioboy @cyberskier I have a docker compose that allows for the following:

  • Setting a custom viewport like 1280x720
  • Setting a custom set of args (remove the // -- and only the // -- from any line you'd like to add to args). Adding // after the first single quote will disable that args setting.
  • Adds the line to go fullscreen if it doesn't exist.
  • Makes video and audio bitrates customizable.

To implement this new Docker Compose, stop the stack then delete the cc4c_config volume, paste in the new compose and make sure you have the environment variables set. Then click "Update the stack"

version: '3.9'
services:
  cc4c:
    image: bnhf/cc4c:${TAG}
    container_name: cc4c
    command:
      - sh 
      - -c 
      - |
        Xvfb :99 -screen 0 1920x1080x16 &
        x11vnc -display :99 -forever &
        sed -i '/width: 1920/c\  width: process.env.WIDTH,' main.js;
        sed -i '/height: 1080/c\  height: process.env.HEIGHT,' main.js;
        if ! grep -q requestFullscreen main.js; then sed -i '319a \          document.documentElement.requestFullscreen();' main.js; fi;
        sed -i -E '/args: \[/,/\],/c\
                args: [\
                  '//\''--enable-unsafe-webgpu'\'',\
                  '//\''--enable-accelerated-video-decode'\'',\
                  '//\''--enable-accelerated-video-encode'\'',\
                  '\''--disable-notifications'\'',\
                  '\''--no-first-run'\'',\
                  '\''--disable-infobars'\'',\
                  '//\''--no-sandbox'\'',\
                  '//\''--use-gl=egl'\'',\
                  '//\''--in-process-gpu'\'',\
                  '\''--hide-crash-restore-bubble'\'',\
                  '\''--disable-blink-features=AutomationControlled'\'',\
                  '\''--hide-scrollbars'\'',\
                  '//\''--enable-features=VaapiVideoEncoder,VaapiVideoDecoder,webgpu,video_encode,gpu_compositing'\'',\
                  '//\''--ignore-gpu-blocklist'\'',\
                  '//\''--disable-features=UseChromeOSDirectVideoDecoder'\'',\
                ],' main.js;
        sed -i '/videoBitsPerSecond/c\        videoBitsPerSecond: process.env.VIDEO,' main.js;
        sed -i '/audioBitsPerSecond/c\        audioBitsPerSecond: process.env.AUDIO,' main.js;
        exec node main.js
    shm_size: '1gb'
    #devices:
      #- /dev/dri:/dev/dri # Uncomment for Intel Quick Sync (GPU) access
    ports:
      - ${HOST_PORT}:5589 # cc4c proxy port
      - ${HOST_VNC_PORT}:5900 # VNC port for entering credentials
    environment:
      - WIDTH=${WIDTH} # Desired viewport width. 1920 suggested.
      - HEIGHT=${HEIGHT} # Desired viewport height. 1080 suggested.
      - VIDEO=${VIDEO} # Desired video streaming rate in bps.
      - AUDIO=${AUDIO} # Desired audio streaming rate in bps.
      - TZ=${TZ} # Add your timezone in the Environment variables section with "name" set to TZ and "value" to your local timezone
    volumes:
      - cc4c:/home/chrome # Creates persistent Docker Volume in /var/lib/docker/volumes for Chrome data and main.js
    restart: unless-stopped
volumes:
  cc4c:
    name: ${HOST_VOLUME} # The filename you'd like Docker to use for the volume.

Sample env vars:

TAG=test
HOST_PORT=5589
HOST_VNC_PORT=5900
WIDTH=1280
HEIGHT=1024
VIDEO=9500000
AUDIO=192000
TZ=US/Mountain
HOST_VOLUME=cc4c_config

@radioboy, I'm not clear on exactly where the Peacock snippet needs to go. Does that code replace the current line 321, or does it go immediately after line 321?

1 Like

bnhf/cc4c:test was just recently built, and has the latest version of Chrome.

Here's my main.js where the Peacock code works as it should and unmutes: (you'll see it near the bottom)

const {launch: puppeteerLaunch} = require('puppeteer-core')
const {launch, getStream} = require('puppeteer-stream')
const fs = require('fs')
const child_process = require('child_process')
const process = require('process')
const path = require('path')
const express = require('express')
const morgan = require('morgan')
require('express-async-errors')
require('console-stamp')(console, {
  format: ':date(yyyy/mm/dd HH:MM:ss.l)',
})

const viewport = {
  width: 1920,
  height: 1080,
}

var currentBrowser, dataDir, lastPage
const getCurrentBrowser = async () => {
  if (!currentBrowser || !currentBrowser.isConnected()) {
    currentBrowser = await launch(
      {
        launch: opts => {
          if (process.pkg) {
            opts.args = opts.args.filter(
              arg => !arg.startsWith('--load-extension=') && !arg.startsWith('--disable-extensions-except=')
            )
            opts.args = opts.args.concat([
              `--load-extension=${path.join(dataDir, 'extension')}`,
              `--disable-extensions-except=${path.join(dataDir, 'extension')}`,
            ])
          }
          if (process.env.DOCKER || process.platform == 'win32') {
            opts.args = opts.args.concat(['--no-sandbox'])
          }
          return puppeteerLaunch(opts)
        },
      },
      {
        executablePath: getExecutablePath(),
        defaultViewport: null, // no viewport emulation
        userDataDir: path.join(dataDir, 'chromedata'),
        args: [
          '--no-first-run',
          '--disable-infobars',
          '--hide-crash-restore-bubble',
          '--disable-blink-features=AutomationControlled',
          '--hide-scrollbars',
        ],
        ignoreDefaultArgs: [
          '--enable-automation',
          '--disable-extensions',
          '--disable-default-apps',
          '--disable-component-update',
          '--disable-component-extensions-with-background-pages',
          '--enable-blink-features=IdleDetection',
        ],
      }
    )
    currentBrowser.on('close', () => {
      currentBrowser = null
    })
    currentBrowser.pages().then(pages => {
      pages.forEach(page => page.close())
    })
  }
  return currentBrowser
}

const getExecutablePath = () => {
  if (process.env.CHROME_BIN) {
    return process.env.CHROME_BIN
  }

  let executablePath
  if (process.platform === 'linux') {
    try {
      executablePath = child_process.execSync('which chromium-browser').toString().split('\n').shift()
    } catch (e) {
      // NOOP
    }

    if (!executablePath) {
      executablePath = child_process.execSync('which chromium').toString().split('\n').shift()
      if (!executablePath) {
        throw new Error('Chromium not found (which chromium)')
      }
    }
  } else if (process.platform === 'darwin') {
    executablePath = [
      '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
      '/Applications/Chromium.app/Contents/MacOS/Chromium',
      '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
    ].find(fs.existsSync)
  } else if (process.platform === 'win32') {
    executablePath = [
      `C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe`,
      `C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe`,
      path.join(process.env.USERPROFILE, 'AppData', 'Local', 'Google', 'Chrome', 'Application', 'chrome.exe'),
      path.join(process.env.USERPROFILE, 'AppData', 'Local', 'Chromium', 'Application', 'chrome.exe'),
    ].find(fs.existsSync)
  } else {
    throw new Error('Unsupported platform: ' + process.platform)
  }

  return executablePath
}

async function main() {
  dataDir = process.cwd()
  if (process.pkg) {
    switch (process.platform) {
      case 'darwin':
        dataDir = path.join(process.env.HOME, 'Library', 'Application Support', 'ChromeCapture')
        break
      case 'win32':
        dataDir = path.join(process.env.USERPROFILE, 'AppData', 'Local', 'ChromeCapture')
        break
    }
    let out = path.join(dataDir, 'extension')
    fs.mkdirSync(out, {recursive: true})
    ;['manifest.json', 'background.js', 'options.html', 'options.js'].forEach(file => {
      fs.copyFileSync(
        path.join(process.pkg.entrypoint, '..', 'node_modules', 'puppeteer-stream', 'extension', file),
        path.join(out, file)
      )
    })
  }

  const app = express()

  const df = require('dateformat')
  morgan.token('mydate', function (req) {
    return df(new Date(), 'yyyy/mm/dd HH:MM:ss.l')
  })
  app.use(morgan('[:mydate] :method :url from :remote-addr responded :status in :response-time ms'))

  app.get('/', (req, res) => {
    res.send(
      `<html>
  <title>Chrome Capture for Channels</title>
  <h2>Chrome Capture for Channels</h2>
  <p>Usage: <code>/stream?url=URL</code> or <code>/stream/&lt;name></code></p>
  <pre>
  #EXTM3U

  #EXTINF:-1 channel-id="windy",Windy
  chrome://${req.get('host')}/stream/windy

  #EXTINF:-1 channel-id="weatherscan",Weatherscan
  chrome://${req.get('host')}/stream/weatherscan
  </pre>
  </html>`
    )
  })

  app.get('/debug', async (req, res) => {
    res.send(`<html>
    <script>
    async function videoClick(e) {
      e.target.focus()
      let x = ((e.clientX-e.target.offsetLeft) * e.target.videoWidth)/e.target.clientWidth
      let y = ((e.clientY-e.target.offsetTop) * e.target.videoHeight)/e.target.clientHeight
      console.log('video click', x, y)
      await fetch('/debug/click/'+x+'/'+y)
    }
    async function videoKeyPress(e) {
      console.log('video keypress', e.key)
      await fetch('/debug/keypress/'+e.key)
    }
    document.addEventListener('keypress', videoKeyPress)
    </script>
    <video style="width: 100%; height: 100%" onKeyPress="videoKeyPress(event)" onClick="videoClick(event)" src="/stream?waitForVideo=false&url=${encodeURIComponent(
      req.query.url || 'https://google.com'
    )}" autoplay muted />
    </html>`)
  })

  app.get('/debug/click/:x/:y', async (req, res) => {
    let browser = await getCurrentBrowser()
    let pages = await browser.pages()
    if (pages.length == 0) {
      res.send('false')
      return
    }
    let page = pages[pages.length - 1]
    await page.mouse.click(parseInt(req.params.x), parseInt(req.params.y))
    res.send('true')
  })

  app.get('/debug/keypress/:key', async (req, res) => {
    let browser = await getCurrentBrowser()
    let pages = await browser.pages()
    if (pages.length == 0) {
      res.send('false')
      return
    }
    let page = pages[pages.length - 1]
    await page.keyboard.press(req.params.key)
    res.send('true')
  })

  app.get('/stream/:name?', async (req, res) => {
    var u = req.query.url
    let name = req.params.name
    if (name) {
      u = {
        nbc: 'https://www.nbc.com/live?brand=nbc&callsign=nbc',
        cnbc: 'https://www.nbc.com/live?brand=cnbc&callsign=cnbc',
        msnbc: 'https://www.nbc.com/live?brand=msnbc&callsign=msnbc',
        nbcnews: 'https://www.nbc.com/live?brand=nbc-news&callsign=nbcnews',
        bravo: 'https://www.nbc.com/live?brand=bravo&callsign=bravo_east',
        bravop: 'https://www.nbc.com/live?brand=bravo&callsign=bravo_west',
        e: 'https://www.nbc.com/live?brand=e&callsign=e_east',
        ep: 'https://www.nbc.com/live?brand=e&callsign=e_west',
        golf: 'https://www.nbc.com/live?brand=golf&callsign=golf',
        oxygen: 'https://www.nbc.com/live?brand=oxygen&callsign=oxygen_east',
        oxygenp: 'https://www.nbc.com/live?brand=oxygen&callsign=oxygen_west',
        syfy: 'https://www.nbc.com/live?brand=syfy&callsign=syfy_east',
        syfyp: 'https://www.nbc.com/live?brand=syfy&callsign=syfy_west',
        usa: 'https://www.nbc.com/live?brand=usa&callsign=usa_east',
        usap: 'https://www.nbc.com/live?brand=usa&callsign=usa_west',
        universo: 'https://www.nbc.com/live?brand=nbc-universo&callsign=universo_east',
        universop: 'https://www.nbc.com/live?brand=nbc-universo&callsign=universo_west',
        necn: 'https://www.nbc.com/live?brand=necn&callsign=necn',
        nbcsbayarea: 'https://www.nbc.com/live?brand=rsn-bay-area&callsign=nbcsbayarea',
        nbcsboston: 'https://www.nbc.com/live?brand=rsn-boston&callsign=nbcsboston',
        nbcscalifornia: 'https://www.nbc.com/live?brand=rsn-california&callsign=nbcscalifornia',
        nbcschicago: 'https://www.nbc.com/live?brand=rsn-chicago&callsign=nbcschicago',
        nbcsphiladelphia: 'https://www.nbc.com/live?brand=rsn-philadelphia&callsign=nbcsphiladelphia',
        nbcswashington: 'https://www.nbc.com/live?brand=rsn-washington&callsign=nbcswashington',
        weatherscan: 'https://weatherscan.net/',
        windy: 'https://windy.com',
      }[name]
    }

    var waitForVideo = req.query.waitForVideo === 'false' ? false : true
    switch (name) {
      case 'weatherscan':
      case 'windy':
        waitForVideo = false
    }
    var minimizeWindow = false
    if (process.platform == 'darwin' && waitForVideo) minimizeWindow = true

    var browser, page
    try {
      browser = await getCurrentBrowser()
      page = await browser.newPage()
      //await page.setBypassCSP(true)
      //page.on('console', msg => console.log(msg.text()))
    } catch (e) {
      console.log('failed to start browser page', u, e)
      res.status(500).send(`failed to start browser page: ${e}`)
      return
    }

    try {
      const stream = await getStream(page, {
        video: true,
        audio: true,
        videoBitsPerSecond: 8000000,
        audioBitsPerSecond: 192000,
        mimeType: 'video/webm;codecs=H264',
        videoConstraints: {
          mandatory: {
            minWidth: viewport.width,
            minHeight: viewport.height,
            maxWidth: viewport.width,
            maxHeight: viewport.height,
            minFrameRate: 60,
          },
        },
      })

      console.log('streaming', u)
      stream.pipe(res)
      res.on('close', async err => {
        await stream.destroy()
        await page.close()
        console.log('finished', u)
      })
    } catch (e) {
      console.log('failed to start stream', u, e)
      res.status(500).send(`failed to start stream: ${e}`)
      await page.close()
      return
    }

    try {
      await page.goto(u)
      if (waitForVideo) {
        await page.waitForSelector('video')
        await page.waitForFunction(`(function() {
          let video = document.querySelector('video')
          return video.readyState === 4
        })()`)
        await page.evaluate(`(function() {
          let video = document.querySelector('video')
          video.style.setProperty('position', 'fixed', 'important')
          video.style.top = '0'
          video.style.left = '0'
          video.style.width = '100%'
          video.style.height = '100%'
          video.style.zIndex = '999000'
          video.style.background = 'black'
          video.style.cursor = 'none'
          video.style.transform = 'translate(0, 0)'
          video.style.objectFit = 'contain'
          video.play()
          video.muted = false
          video.removeAttribute('muted')

          let header = document.querySelector('.header-container')
          if (header) {
            header.style.zIndex = '0'
          }
        })()`)
      }
    if (page.url().includes('peacocktv.com')) {
        await page.waitForSelector('.playback-button-icon.volume-muted', { visible: true }); // Wait for the element to be visible

        await page.evaluate(() => {
            const svgElement = document.querySelector('.playback-button-icon.volume-muted');
            if (svgElement) {
                svgElement.dispatchEvent(new MouseEvent('click', { bubbles: true })); // Dispatch a click event on the SVG element
            } else {
                console.error('SVG element not found');
            }
        });
    }
      const uiSize = await page.evaluate(`(function() {
        return {
          height: window.outerHeight - window.innerHeight,
          width: window.outerWidth - window.innerWidth,
        }
      })()`)
      const session = await page.target().createCDPSession()
      const {windowId} = await session.send('Browser.getWindowForTarget')
      await session.send('Browser.setWindowBounds', {
        windowId,
        bounds: {
          height: viewport.height + uiSize.height,
          width: viewport.width + uiSize.width,
        },
      })
      if (minimizeWindow) {
        await session.send('Browser.setWindowBounds', {
          windowId,
          bounds: {
            windowState: 'minimized',
          },
        })
      }
    } catch (e) {
      console.log('failed to stream', u, e)
    }
  })

  const server = app.listen(5589, () => {
    console.log('Chrome Capture server listening on port 5589')
  })
}

main()

I am getting an error:

Blockquote
failed to deploy a stack: cc4c Pulling cc4c Error Error response from daemon: manifest for bnhf/cc4c:latest not found: manifest unknown: manifest unknown

TAG=test

I'm a moron - I JUST came here to say that LOL... deploying now. Did my post with the main.js and the peacock code help?

I don't use cc4c, but what I meant was you should set the Environment variable to
HOST_VOLUME=/volume1/docker/cc4c
That way the main.js file will be easier to access there /volume1/docker/cc4c/main.js

1 Like

Yes. Working on it now.

@bnhf Thank you!! - FYI, I tried launching one of the Peacock channels and it just loads a black screen... Weatherscan is still working OK.

Good idea, but in this case we need the contents of the directory copied from the container -- which is the default with Docker Volumes. When you do a directory binding, it starts empty and you need to specifically copy the contents over. Not difficult to do, but I've been trying to keep the compose and container compatible with the original fancybits image.

2 Likes

I restarted the new test stack and Peacock still generates the "Something went wrong." screen with dialog that the system configuration is not compatible. I guess they can see what type of system is trying to access the stream, even though it's Chrome 127 now... hrm. They're annoying LOL.