Преглед на файлове

bitrate histogram, channel details

Joystream Stats преди 2 години
родител
ревизия
cb10c2efd2
променени са 6 файла, в които са добавени 263 реда и са изтрити 69 реда
  1. 2 1
      package.json
  2. 33 0
      src/components/Media/Histogram.tsx
  3. 49 0
      src/components/Media/Videos.tsx
  4. 31 64
      src/components/Media/index.tsx
  5. 135 0
      src/components/Modals/Player.tsx
  6. 13 4
      src/components/Modals/index.tsx

+ 2 - 1
package.json

@@ -23,6 +23,7 @@
     "react-beautiful-dnd": "^13.1.0",
     "react-bootstrap": "^1.4.0",
     "react-calendar-timeline": "^0.27.0",
+    "react-chart-histogram": "^0.2.4",
     "react-dom": "^17.0.1",
     "react-feather": "^2.0.9",
     "react-horizontal-timeline": "^1.5.3",
@@ -33,7 +34,7 @@
     "react-scripts": "4.0.1",
     "remark-gfm": "^1.0.0",
     "styled-components": "^5.3.0",
-    "video-react": "^0.14.1"
+    "video-react": "^0.15.0"
   },
   "scripts": {
     "start": "HOST=localhost PORT=3030 react-scripts start",

+ 33 - 0
src/components/Media/Histogram.tsx

@@ -0,0 +1,33 @@
+import Histogram from 'react-chart-histogram'; 
+
+const Graph = (props : {}) => {
+  const {objects, label, show} = props
+  if (!show) return <div/>
+  // plot count per bitrate range
+  const max = objects.reduce((max,o) => o.bitrate < max ? max : o.bitrate, 0)
+   console.debug(`max`,max, objects.count)
+  let labels = [];
+  let data = [];
+  const step = 100000
+  for (let i = step ; i < 1500000; i += step) {
+    const count = objects.filter(o=> o.bitrate > i-step && o.bitrate < i  ).length
+const label = i < 1000000 ? (i/1000).toFixed() +"K"  : (i/1000**2).toFixed(1) + "M"
+    labels.push(label)
+    data.push(count)
+  }
+  const options = { fillColor: '#FFFFFF', strokeColor: '#0000FF' };
+  return (
+    <div>
+      <Histogram
+          xLabels={labels}
+          yValues={data}
+          width='600'
+          height='200'
+          options={options}
+      />
+      <div className='mb-2 text-secondary'>{label}</div>
+    </div>
+  )
+}
+
+export default Graph

+ 49 - 0
src/components/Media/Videos.tsx

@@ -0,0 +1,49 @@
+import Histogram from "./Histogram";
+
+const Videos = (props:{}) => {
+  const {selectVideo, objects, page,perPage, showChart} = props 
+  return (
+    <>
+      <Histogram show={showChart} objects={objects} label="videos with bitrate per second" />
+      <hr/>
+      <div className="d-flex flex-wrap">
+        {objects
+          .slice((page - 1) * perPage, page * perPage)
+          .map((o) => (
+            <Video key={o.id} selectVideo={selectVideo} {...o} />
+          ))}
+      </div>
+    </>
+  );
+};
+
+export default Videos;
+
+const Video = (props: {}) => {
+  const { selectVideo, id, videoMedia, bitrate, providers, size } = props;
+  if (!providers?.length) return "";
+  
+  const alt = `${id} ${videoMedia.title}`;  
+  const url = providers[0].operators[0].metadata.nodeEndpoint;
+  
+  return (
+    <div
+      key={videoMedia.id}
+      className="text-left p-1"
+      style={{ width: "200px" }}
+      onClick={() => selectVideo(id)}
+    >
+      <img
+        className="d-block p-1"
+        style={{ width: "200px" }}
+        src={url + "api/v1/assets/" + videoMedia.thumbnailPhotoId}
+        alt={alt}
+        title={alt}
+      />
+      <div>
+        {(bitrate / 1024 ** 2).toFixed(2)} mps / {(size / 1024 ** 2).toFixed()}
+        MB / {videoMedia.duration}s
+      </div>
+    </div>
+  );
+};

+ 31 - 64
src/components/Media/index.tsx

@@ -1,27 +1,30 @@
 import axios from "axios";
 import { useEffect, useState } from "react";
-import { PlusSquare, MinusSquare } from "react-feather";
+import { BarChart2, PlusSquare, MinusSquare } from "react-feather";
 import { queryNode } from "../../config";
+import Videos from "./Videos";
 
 const query = `query {
-  storageBags { id
+  channels (limit: 10000) { id title ownerMemberId }
+  memberships(limit: 10000) { id handle channels {id} ownedNfts {id} }
+  storageBags { id 
     distributionBuckets { operators { metadata{nodeEndpoint } } }
     objects { id size  
-      videoMedia {id categoryId isCensored     isExplicit    isFeatured isPublic
-        thumbnailPhotoId duration title description
-        mediaMetadata {pixelWidth pixelHeight size encoding {codecName}}
-      }
-    }
-  }
+      videoMedia {id categoryId
+      isCensored     isExplicit    isFeatured    isPublic
+      thumbnailPhotoId duration title description
+      mediaMetadata {pixelWidth pixelHeight size encoding {codecName} } }               
+    }  }
 }`;
 
 const Media = (props: {}) => {
   const { save, selectVideo, media } = props;
   const [page, setPage] = useState(1);
   const [perPage, setPerPage] = useState(50);
+  const [showChart, setShowChart] = useState(false);
 
   useEffect(() => {
-    media?.storageBags?.length ||
+    media?.channels ||
       axios
         .post(queryNode, { query })
         .then(({ data }) => save("media", data.data))
@@ -31,68 +34,32 @@ const Media = (props: {}) => {
   return (
     <div className="box">
       <h2>
-        <MinusSquare onClick={() => page > 1 && setPage(page - 1)}>
-          -
-        </MinusSquare>{" "}
+      <BarChart2 className="float-right" onClick={()=>setShowChart(!showChart)}/>
+        <MinusSquare
+          onClick={() => page > 1 && setPage(page - 1)}
+          disabled={page === 1}
+        />
         Media
-        <PlusSquare className="ml-1" onClick={() => setPage(page + 1)}>
-          +
-        </PlusSquare>
+        <PlusSquare className="ml-1" onClick={() => setPage(page + 1)} />
       </h2>
       {media.storageBags?.length ? (
-        <div className="d-flex flex-wrap">
-          {media.storageBags
-            .reduce((objects, b) => {
-              b.objects.map((o) =>
-                objects.push({
-                  ...o,
-                  providers: b.distributionBuckets,
-                  bitrate: o.videoMedia?.duration
-                    ? (o.size / o.videoMedia.duration).toFixed()
-                    : 0,
-                })
-              );
-              return objects;
-            }, [])
-            .filter((o) => o.bitrate)
-            .sort((a, b) => b.bitrate - a.bitrate)
-            .slice((page - 1) * perPage, page * perPage)
-            .map((o) => (
-              <Video key={o.id} selectVideo={selectVideo} {...o} />
-            ))}
-        </div>
+      <Videos showChart={showChart} selectVideo={selectVideo} perPage={perPage} page={page} objects={media.storageBags
+          .reduce((objects, b) => {          
+            b.objects.forEach((o) => {
+              if (!o.videoMedia?.duration) return //console.debug(`skipping`,o)
+              const bitrate = (o.size / o.videoMedia.duration).toFixed()
+              const obj = {...o, providers: b.distributionBuckets, bitrate }
+              objects.push(obj)
+            })
+            return objects
+          }, [])
+          .filter((o) => o.bitrate)
+          .sort((a, b) => b.bitrate - a.bitrate)} />
       ) : (
-        ""
+        "Waiting for results from QN .."
       )}
     </div>
   );
 };
 
-const Video = (props: {}) => {
-  const { selectVideo, id, videoMedia, bitrate, providers, size } = props;
-  const alt = `${id} ${videoMedia.title}`;
-  if (!providers?.length) return "";
-  const url = providers[0].operators[0].metadata.nodeEndpoint;
-  return (
-    <div
-      key={videoMedia.id}
-      className="text-left p-1"
-      style={{ width: "200px" }}
-      onClick={() => selectVideo(id)}
-    >
-      <img
-        className="d-block p-1"
-        style={{ width: "200px" }}
-        src={url + "api/v1/assets/" + videoMedia.thumbnailPhotoId}
-        alt={alt}
-        title={alt}
-      />
-      <div>
-        {(bitrate / 1024 ** 2).toFixed(2)} mps / {(size / 1024 ** 2).toFixed()}
-        MB / {videoMedia.duration}s
-      </div>
-    </div>
-  );
-};
-
 export default Media;

+ 135 - 0
src/components/Modals/Player.tsx

@@ -0,0 +1,135 @@
+import { useEffect, useState } from "react";
+import { Badge, Button, Modal } from "react-bootstrap";
+import { Player } from "video-react";
+import { PlusSquare, MinusSquare } from "react-feather";
+import "video-react/dist/video-react.css";
+import axios from "axios";
+
+const PlayerModal = (props) => {
+  const { id, channel, selectVideo } = props;
+
+  const [latencies, setLatencies] = useState([]);
+  const [provider, setProvider] = useState({});
+  const [showLatencies, toggleShowLatencies] = useState(false);
+
+  useEffect(() => {
+    let latencies = [];
+    channel?.distributionBuckets?.forEach((b) => {
+      const url = b.operators[0]?.metadata.nodeEndpoint;
+      const start = new Date();
+      axios
+        //        .get(url + "api/v1/status")
+        .head(url + `api/v1/assets/${id}`)
+        .then(({ data }) => {
+          latencies.push({ url, latency: new Date() - start });
+          setLatencies(latencies.sort((a, b) => a.latency - b.latency));
+          if (latencies[0]) setProvider(latencies[0]);
+        })
+        .catch((e) => console.error(url, e.message));
+    });
+  }, [id, channel.distributionBuckets]);
+
+  const video = channel.objects.find((o) => +o.id === +id);
+  const { title, description, duration } = video.videoMedia;
+  const videoUrl = provider?.url + `api/v1/assets/${id}`;
+  const bitrate = video.size / duration / 1024 ** 2;
+
+  return (
+    <Modal
+      size="lg"
+      aria-labelledby="contained-modal-title-vcenter"
+      centered
+      show={id && channel && video}
+      onHide={() => selectVideo()}
+    >
+      <Modal.Header
+        className="no-border overflow-hidden bg-dark text-light"
+        closeButton
+      >
+        <Modal.Title
+          id="contained-modal-title-vcenter"
+          title={`[${id}] ${title}`}
+          className="text-nowrap overflow-hidden"
+        >
+          {title}
+        </Modal.Title>
+      </Modal.Header>
+      <Modal.Body className="text-center bg-dark text-light">
+        {latencies.length ? (
+          <>
+            <Player autoPlay playsInline src={videoUrl}></Player>
+            <div className='box float-right'>
+              <a className="font-weight-bold" href={`https://play.joystream.org/channel/${channel.id}`}>{channel.title}</a>
+              <br/>by <a href={`https://play.joystream.org/member/${channel.owner.handle}?tab=About`}>{channel.owner.handle}</a>
+            </div>
+            <div className="d-flex flex-column text-left my-2">
+              <div>
+                Bitrate: {bitrate.toFixed(2)} mps (
+                {(video.size / 1024 ** 2).toFixed()} MB / {duration} s)
+              </div>
+              <div>
+                Provider:{" "}
+                <Badge>
+                  {provider.url} ({provider.latency}ms)
+                </Badge>
+                <Button
+                  variant="dark"
+                  className="btn-sm m-1 p-0"
+                  onClick={() => toggleShowLatencies(!showLatencies)}
+                >
+                  {showLatencies ? <MinusSquare /> : <PlusSquare />}
+                </Button>
+              </div>
+            </div>
+            {showLatencies ? (
+              <div className="d-flex flex-column">
+                {latencies.map((l) => (
+                  <div
+                    key={l.url}
+                    className="d-flex flex-row"
+                    onClick={() => setProvider(l)}
+                  >
+                    <Badge>{l.latency} ms</Badge>
+                    <Badge>{l.url}</Badge>
+                  </div>
+                ))}
+              </div>
+            ) : (
+              ""
+            )}
+          </>
+        ) : (
+          "Finding best provider .."
+        )}
+      </Modal.Body>
+      <Modal.Footer className="bg-dark text-light d-flex flex-row justify-content-start">
+        <div className="d-flex flex-column">
+          <div className="font-weight-bold">{title}</div>
+          <div>{description}</div>
+        </div>
+      </Modal.Footer>
+    </Modal>
+  );
+};
+
+export default PlayerModal;
+
+const SelectProvider = (props: {
+  handleChange: () => void;
+  me: { providers: { [key: string]: string[] }; provider: string };
+}) => {
+  const { handleChange, me } = props;
+  const regions = Object.keys(me.providers);
+  return (
+    <select
+      className="form-control text-center bg-dark text-secondary"
+      name="provider"
+      value={me.provider}
+      onChange={handleChange}
+    >
+      {regions.map((r) =>
+        me.providers[r].map((p) => <option key={p}>{p}</option>)
+      )}
+    </select>
+  );
+};

+ 13 - 4
src/components/Modals/index.tsx

@@ -6,6 +6,18 @@ import Player from "./Player";
 const Modals = (props) => {
   const { editKpi, toggleEditKpi, showModal, toggleShowStatus, showStatus } =
     props;
+
+const getChannel = (objectId) => {
+   const {storageBags=[], channels=[], memberships=[]} = props.media
+   const bag = props.media.storageBags.find((b) =>
+            b.objects.find((o) => +o.id === +props.video)
+          )
+   const channelId = bag.id.split(":")[2]
+   const channel = channels.find(c=> +c.id === +channelId)
+   const owner = memberships.find(m => +m.id === +channel?.ownerMemberId)
+   return {...bag,...channel, owner}
+}
+
   return (
     <div>
       {props.video ? (
@@ -13,10 +25,7 @@ const Modals = (props) => {
           id={props.video}
           onHide={props.selectVideo}
           selectVideo={props.selectVideo}
-          channel={props.media.storageBags.find((b) =>
-            b.objects.find((o) => +o.id === +props.video)
-          )}
-        />
+          channel={getChannel(props.video)} />
       ) : props.selectedEvent ? (
         <Event event={props.selectedEvent} onHide={props.selectEvent} />
       ) : editKpi ? (