@financial-times/cp-content-pipeline-schema 3.17.0 → 3.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/lib/generated/index.d.ts +23 -2
  3. package/lib/helpers/qualityWorkaround.d.ts +2 -0
  4. package/lib/helpers/qualityWorkaround.js +37 -0
  5. package/lib/helpers/qualityWorkaround.js.map +1 -0
  6. package/lib/model/Clip.d.ts +1 -17
  7. package/lib/model/Clip.js +27 -8
  8. package/lib/model/Clip.js.map +1 -1
  9. package/lib/model/Clip.test.js +77 -0
  10. package/lib/model/Clip.test.js.map +1 -1
  11. package/lib/model/Topper.d.ts +1 -0
  12. package/lib/model/Topper.js +6 -1
  13. package/lib/model/Topper.js.map +1 -1
  14. package/lib/model/schemas/capi/article.d.ts +18 -0
  15. package/lib/model/schemas/capi/audio.d.ts +14 -0
  16. package/lib/model/schemas/capi/base-schema.d.ts +42 -0
  17. package/lib/model/schemas/capi/base-schema.js +2 -0
  18. package/lib/model/schemas/capi/base-schema.js.map +1 -1
  19. package/lib/model/schemas/capi/content-package.d.ts +14 -0
  20. package/lib/model/schemas/capi/custom-code-component.d.ts +18 -0
  21. package/lib/model/schemas/capi/index.d.ts +96 -0
  22. package/lib/model/schemas/capi/live-blog-package.d.ts +18 -0
  23. package/lib/model/schemas/capi/placeholder.d.ts +18 -0
  24. package/lib/model/schemas/capi/video.d.ts +14 -0
  25. package/lib/resolvers/clip.d.ts +4 -9
  26. package/lib/resolvers/clip.js +3 -0
  27. package/lib/resolvers/clip.js.map +1 -1
  28. package/lib/resolvers/index.d.ts +6 -9
  29. package/lib/resolvers/topper.d.ts +2 -0
  30. package/lib/resolvers/topper.js +4 -2
  31. package/lib/resolvers/topper.js.map +1 -1
  32. package/lib/types/clip.d.ts +21 -0
  33. package/lib/types/clip.js +3 -0
  34. package/lib/types/clip.js.map +1 -0
  35. package/package.json +1 -1
  36. package/queries/article.graphql +6 -0
  37. package/src/generated/index.ts +23 -2
  38. package/src/helpers/qualityWorkaround.ts +44 -0
  39. package/src/model/Clip.test.ts +95 -0
  40. package/src/model/Clip.ts +33 -27
  41. package/src/model/Topper.ts +7 -1
  42. package/src/model/schemas/capi/base-schema.ts +2 -0
  43. package/src/resolvers/clip.ts +3 -0
  44. package/src/resolvers/topper.ts +4 -2
  45. package/src/types/clip.ts +23 -0
  46. package/tsconfig.tsbuildinfo +1 -1
  47. package/typedefs/clip.graphql +9 -0
  48. package/typedefs/topper.graphql +8 -2
@@ -17,6 +17,9 @@ const resolvers = {
17
17
  pixelWidth: (parent) => parent.pixelWidth ?? null,
18
18
  pixelHeight: (parent) => parent.pixelHeight ?? null,
19
19
  videoCodec: (parent) => parent.videoCodec ?? null,
20
+ quality: (parent) => parent.quality ?? null,
21
+ dppx: (parent) => parent.dppx ?? null,
22
+ previousSourceWidth: (parent) => parent.previousSourceWidth ?? null,
20
23
  },
21
24
  };
22
25
  exports.default = resolvers;
@@ -1 +1 @@
1
- {"version":3,"file":"clip.js","sourceRoot":"","sources":["../../src/resolvers/clip.ts"],"names":[],"mappings":";;AAEA,MAAM,SAAS,GAAG;IAChB,IAAI,EAAE;QACJ,aAAa,EAAE,GAAG,EAAE,CAAC,MAAM;QAC3B,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE;QACvC,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE;QAC3B,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE;QAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE;QAC/B,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE;KACxB;IAED,UAAU,EAAE;QACV,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,IAAI,IAAI;QACjD,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS;QACvC,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,IAAI,IAAI;QAC7C,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS;QACvC,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,IAAI,IAAI;QACjD,WAAW,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,IAAI,IAAI;QACnD,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,IAAI,IAAI;KAClD;CACiE,CAAA;AAEpE,kBAAe,SAAS,CAAA"}
1
+ {"version":3,"file":"clip.js","sourceRoot":"","sources":["../../src/resolvers/clip.ts"],"names":[],"mappings":";;AAEA,MAAM,SAAS,GAAG;IAChB,IAAI,EAAE;QACJ,aAAa,EAAE,GAAG,EAAE,CAAC,MAAM;QAC3B,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE;QACvC,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE;QAC3B,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE;QAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE;QAC/B,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE;KACxB;IAED,UAAU,EAAE;QACV,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,IAAI,IAAI;QACjD,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS;QACvC,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,IAAI,IAAI;QAC7C,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS;QACvC,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,IAAI,IAAI;QACjD,WAAW,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,IAAI,IAAI;QACnD,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,IAAI,IAAI;QACjD,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,IAAI,IAAI;QAC3C,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,IAAI,IAAI;QACrC,mBAAmB,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,mBAAmB,IAAI,IAAI;KACpE;CACiE,CAAA;AAEpE,kBAAe,SAAS,CAAA"}
@@ -61,6 +61,7 @@ declare const resolvers: {
61
61
  isLargeHeadline: (topper: import("../model/Topper").Topper) => boolean;
62
62
  layout: (topper: import("../model/Topper").Topper) => string;
63
63
  columnist: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept | null;
64
+ columnists: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept[] | null;
64
65
  __resolveType?: import("../generated").TypeResolveFn<"DeepPortraitTopper" | "DeepLandscapeTopper" | "SplitTextTopper" | "FullBleedTopper" | "PodcastTopper" | "OpinionTopper" | "BrandedTopper" | "BasicTopper" | "TopperWithFlourish" | "PartnerContentTopper", import("../model/Topper").Topper, import("..").QueryContext> | undefined;
65
66
  backgroundBox: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<boolean>>, import("../model/Topper").Topper, import("..").QueryContext, {}>;
66
67
  backgroundColour: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<"paper" | "wheat" | "white" | "black" | "claret" | "oxford" | "slate" | "crimson" | "sky" | "matisse">>, import("../model/Topper").Topper, import("..").QueryContext, {}>;
@@ -76,6 +77,7 @@ declare const resolvers: {
76
77
  isLargeHeadline: (topper: import("../model/Topper").Topper) => boolean;
77
78
  layout: (topper: import("../model/Topper").Topper) => string;
78
79
  columnist: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept | null;
80
+ columnists: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept[] | null;
79
81
  __resolveType?: import("../generated").TypeResolveFn<"DeepPortraitTopper" | "DeepLandscapeTopper" | "SplitTextTopper" | "FullBleedTopper" | "PodcastTopper" | "OpinionTopper" | "BrandedTopper" | "BasicTopper" | "TopperWithFlourish" | "PartnerContentTopper", import("../model/Topper").Topper, import("..").QueryContext> | undefined;
80
82
  backgroundBox: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<boolean>>, import("../model/Topper").Topper, import("..").QueryContext, {}>;
81
83
  backgroundColour: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<"paper" | "wheat" | "white" | "black" | "claret" | "oxford" | "slate" | "crimson" | "sky" | "matisse">>, import("../model/Topper").Topper, import("..").QueryContext, {}>;
@@ -342,15 +344,7 @@ declare const resolvers: {
342
344
  MetaLink: import("../generated").MetaLinkResolvers;
343
345
  Clip: {
344
346
  __resolveType: () => string;
345
- dataSource: (clip: import("../model/Clip").Clip) => {
346
- binaryUrl: string;
347
- mediaType: string;
348
- audioCodec?: string;
349
- duration?: number;
350
- pixelHeight?: number;
351
- pixelWidth?: number;
352
- videoCodec?: string;
353
- }[];
347
+ dataSource: (clip: import("../model/Clip").Clip) => import("../types/clip").ClipSource[];
354
348
  type: (clip: import("../model/Clip").Clip) => string;
355
349
  format: (clip: import("../model/Clip").Clip) => "mobile" | "standard-inline";
356
350
  poster: (clip: import("../model/Clip").Clip) => string;
@@ -364,6 +358,9 @@ declare const resolvers: {
364
358
  pixelWidth: (parent: import("../generated").ClipSource) => number | null;
365
359
  pixelHeight: (parent: import("../generated").ClipSource) => number | null;
366
360
  videoCodec: (parent: import("../generated").ClipSource) => string | null;
361
+ quality: (parent: import("../generated").ClipSource) => string | null;
362
+ dppx: (parent: import("../generated").ClipSource) => number | null;
363
+ previousSourceWidth: (parent: import("../generated").ClipSource) => number | null;
367
364
  };
368
365
  Image: {
369
366
  __resolveType: import("../generated").TypeResolveFn<"ImageDesktop" | "ImageLandscape" | "ImageMobile" | "ImagePortrait" | "ImageSquare" | "ImageSquareFTEdit" | "ImageStandard" | "ImageStandardInline" | "ImageWide", import("../model/Image").Image, import("..").QueryContext>;
@@ -31,6 +31,7 @@ declare const resolvers: {
31
31
  isLargeHeadline: (topper: import("../model/Topper").Topper) => boolean;
32
32
  layout: (topper: import("../model/Topper").Topper) => string;
33
33
  columnist: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept | null;
34
+ columnists: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept[] | null;
34
35
  __resolveType?: import("../generated").TypeResolveFn<"DeepPortraitTopper" | "DeepLandscapeTopper" | "SplitTextTopper" | "FullBleedTopper" | "PodcastTopper" | "OpinionTopper" | "BrandedTopper" | "BasicTopper" | "TopperWithFlourish" | "PartnerContentTopper", import("../model/Topper").Topper, import("..").QueryContext> | undefined;
35
36
  backgroundBox: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<boolean>>, import("../model/Topper").Topper, import("..").QueryContext, {}>;
36
37
  backgroundColour: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<"paper" | "wheat" | "white" | "black" | "claret" | "oxford" | "slate" | "crimson" | "sky" | "matisse">>, import("../model/Topper").Topper, import("..").QueryContext, {}>;
@@ -46,6 +47,7 @@ declare const resolvers: {
46
47
  isLargeHeadline: (topper: import("../model/Topper").Topper) => boolean;
47
48
  layout: (topper: import("../model/Topper").Topper) => string;
48
49
  columnist: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept | null;
50
+ columnists: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept[] | null;
49
51
  __resolveType?: import("../generated").TypeResolveFn<"DeepPortraitTopper" | "DeepLandscapeTopper" | "SplitTextTopper" | "FullBleedTopper" | "PodcastTopper" | "OpinionTopper" | "BrandedTopper" | "BasicTopper" | "TopperWithFlourish" | "PartnerContentTopper", import("../model/Topper").Topper, import("..").QueryContext> | undefined;
50
52
  backgroundBox: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<boolean>>, import("../model/Topper").Topper, import("..").QueryContext, {}>;
51
53
  backgroundColour: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<"paper" | "wheat" | "white" | "black" | "claret" | "oxford" | "slate" | "crimson" | "sky" | "matisse">>, import("../model/Topper").Topper, import("..").QueryContext, {}>;
@@ -36,14 +36,16 @@ const resolvers = {
36
36
  headshot: (topper, args) => topper.headshot(args),
37
37
  isLargeHeadline: (topper) => topper.isLargeHeadline(),
38
38
  layout: (topper) => topper.layout(),
39
- columnist: (topper) => topper.columnist(),
39
+ columnist: (topper) => topper.columnist(), // @deprecated Replaced with usage of `columinists`
40
+ columnists: (topper) => topper.columnists(),
40
41
  },
41
42
  OpinionTopper: {
42
43
  ...topperResolvers,
43
44
  headshot: (topper, args) => topper.headshot(args),
44
45
  isLargeHeadline: (topper) => topper.isLargeHeadline(),
45
46
  layout: (topper) => topper.layout(),
46
- columnist: (topper) => topper.columnist(),
47
+ columnist: (topper) => topper.columnist(), // @deprecated Replaced with usage of `columinists`
48
+ columnists: (topper) => topper.columnists(),
47
49
  },
48
50
  TopperWithPackage: {
49
51
  design: (topper) => topper.design().theme,
@@ -1 +1 @@
1
- {"version":3,"file":"topper.js","sourceRoot":"","sources":["../../src/resolvers/topper.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"topper.js","sourceRoot":"","sources":["../../src/resolvers/topper.ts"],"names":[],"mappings":";;AAmBA,MAAM,eAAe,GAAoB;IACvC,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,EAAE;IACjD,gBAAgB,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,gBAAgB,EAAE;IACvD,cAAc,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,cAAc,EAAE;IACnD,mBAAmB,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,mBAAmB,EAAE;IAC7D,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE;IAC/C,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE;IACvC,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE;IACjC,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE;CAC5C,CAAA;AAED,MAAM,SAAS,GAAG;IAChB,MAAM,EAAE;QACN,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE;QACxC,GAAG,eAAe;KACnB;IAED,gBAAgB,EAAE;QAChB,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;QACnC,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,EAAE;KAClD;IAED,eAAe,EAAE;QACf,eAAe,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,eAAe,EAAE;QACrD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;KACpC;IAED,eAAe,EAAE;QACf,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE;QAC/C,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE;KAChD;IAED,kBAAkB,EAAE;QAClB,QAAQ,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;KAClD;IAED,aAAa,EAAE;QACb,GAAG,eAAe;QAClB,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE;QAC/C,QAAQ,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;QACjD,eAAe,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,eAAe,EAAE;QACrD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;QACnC,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,mDAAmD;QAC9F,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE;KAC5C;IAED,aAAa,EAAE;QACb,GAAG,eAAe;QAClB,QAAQ,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;QACjD,eAAe,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,eAAe,EAAE;QACrD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;QACnC,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,mDAAmD;QAC9F,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE;KAC5C;IAED,iBAAiB,EAAE;QACjB,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK;KAC1C;IAED,WAAW,EAAE;QACX,GAAG,eAAe;KACnB;IAED,aAAa,EAAE;QACb,GAAG,eAAe;QAClB,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE;QAC/C,eAAe,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,eAAe,EAAE;QACrD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;KACpC;IAED,mBAAmB,EAAE;QACnB,GAAG,eAAe;QAClB,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE;QAC/C,eAAe,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,eAAe,EAAE;QACrD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;QACnC,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,EAAE;QACjD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;KACpC;IAED,kBAAkB,EAAE;QAClB,GAAG,eAAe;QAClB,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE;QAC/C,eAAe,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,eAAe,EAAE;QACrD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;QACnC,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,EAAE;QACjD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;KACpC;IAED,kBAAkB,EAAE;QAClB,GAAG,eAAe;QAClB,eAAe,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,eAAe,EAAE;QACrD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;QACnC,WAAW,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE;QAC7C,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE;KAChD;IAED,eAAe,EAAE;QACf,GAAG,eAAe;QAClB,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE;QAC/C,eAAe,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,eAAe,EAAE;QACrD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;QACnC,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,EAAE;QACjD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;KACpC;IAED,eAAe,EAAE;QACf,GAAG,eAAe;QAClB,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK;QACzC,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE;QAC/C,eAAe,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,eAAe,EAAE;QACrD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;QACnC,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,EAAE;QACjD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;KACpC;IAED,oBAAoB,EAAE;QACpB,GAAG,eAAe;QAClB,eAAe,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,eAAe,EAAE;QACrD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;QACnC,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,EAAE;QACjD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;KACpC;CAkBF,CAAA;AAED,kBAAe,SAAS,CAAA"}
@@ -0,0 +1,21 @@
1
+ import { LiteralUnionScalarValues } from '../resolvers/literal-union';
2
+ import { ClipFormat } from '../resolvers/scalars';
3
+ export type ClipSource = {
4
+ binaryUrl: string;
5
+ mediaType: string;
6
+ audioCodec?: string;
7
+ duration?: number;
8
+ pixelHeight?: number;
9
+ pixelWidth?: number;
10
+ videoCodec?: string;
11
+ quality?: string;
12
+ dppx?: number;
13
+ previousSourceWidth?: number;
14
+ };
15
+ export interface ClipVideo {
16
+ id(): string;
17
+ type(): string;
18
+ format(): LiteralUnionScalarValues<typeof ClipFormat>;
19
+ poster(): string;
20
+ dataSource(): ClipSource[];
21
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=clip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clip.js","sourceRoot":"","sources":["../../src/types/clip.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/cp-content-pipeline-schema",
3
- "version": "3.17.0",
3
+ "version": "3.19.0",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -199,6 +199,9 @@ fragment Topper on Topper {
199
199
  columnist {
200
200
  ...Concept
201
201
  }
202
+ columnists {
203
+ ...Concept
204
+ }
202
205
  }
203
206
  ... on PodcastTopper {
204
207
  headshot(dpr: 2, width: 160)
@@ -355,6 +358,9 @@ fragment Clip on Clip {
355
358
  pixelHeight
356
359
  pixelWidth
357
360
  videoCodec
361
+ quality
362
+ dppx
363
+ previousSourceWidth
358
364
  }
359
365
  poster
360
366
  }
@@ -377,6 +377,8 @@ export type ClipSource = {
377
377
  readonly audioCodec?: Maybe<Scalars['String']['output']>;
378
378
  /** The url of the clip source, eg. 'https://next-media-api.ft.com/renditions/16868569859480/0x0.mp3'. */
379
379
  readonly binaryUrl: Scalars['String']['output'];
380
+ /** The encoding settings intention for the video media */
381
+ readonly dppx?: Maybe<Scalars['Int']['output']>;
380
382
  /** The duration of the clip in milliseconds. */
381
383
  readonly duration?: Maybe<Scalars['Int']['output']>;
382
384
  /** The media type eg. video/mp4. */
@@ -385,6 +387,10 @@ export type ClipSource = {
385
387
  readonly pixelHeight?: Maybe<Scalars['Int']['output']>;
386
388
  /** The width of the clip in pixels. */
387
389
  readonly pixelWidth?: Maybe<Scalars['Int']['output']>;
390
+ /** The width of the source immediately narrower than this one. */
391
+ readonly previousSourceWidth?: Maybe<Scalars['Int']['output']>;
392
+ /** The encoding quality of the video media */
393
+ readonly quality?: Maybe<Scalars['String']['output']>;
388
394
  /** The video encoding format of the source, eg. h264. */
389
395
  readonly videoCodec?: Maybe<Scalars['String']['output']>;
390
396
  };
@@ -1318,8 +1324,13 @@ export type OpinionTopper = Topper & TopperWithHeadshot & TopperWithTheme & {
1318
1324
  readonly backgroundBox?: Maybe<Scalars['Boolean']['output']>;
1319
1325
  /** The background colour of the topper. */
1320
1326
  readonly backgroundColour?: Maybe<Scalars['TopperBackgroundColour']['output']>;
1321
- /** The concept object describing details of the column author. */
1327
+ /**
1328
+ * The concept object describing details of the column author.
1329
+ * @deprecated use columnists instead
1330
+ */
1322
1331
  readonly columnist?: Maybe<Concept>;
1332
+ /** The concept object describing details of the column author(s). */
1333
+ readonly columnists?: Maybe<ReadonlyArray<Concept>>;
1323
1334
  /** The concept object to be displayed, eg. {'type': 'TOPIC', 'prefLabel': 'Non-dom tax status', ...}. */
1324
1335
  readonly displayConcept?: Maybe<Concept>;
1325
1336
  /** The variant of the follow button to be displayed on the topper. */
@@ -1549,8 +1560,13 @@ export type PodcastTopper = Topper & TopperWithBrand & TopperWithHeadshot & Topp
1549
1560
  readonly backgroundColour?: Maybe<Scalars['TopperBackgroundColour']['output']>;
1550
1561
  /** The concept object of the brand, eg. {'type': 'BRAND', 'prefLabel': 'FT Magazine', ...}. */
1551
1562
  readonly brandConcept?: Maybe<Concept>;
1552
- /** The concept object describing details of the column author. */
1563
+ /**
1564
+ * The concept object describing details of the column author.
1565
+ * @deprecated use columnists instead
1566
+ */
1553
1567
  readonly columnist?: Maybe<Concept>;
1568
+ /** The concept object describing details of the column author(s). */
1569
+ readonly columnists?: Maybe<ReadonlyArray<Concept>>;
1554
1570
  /** The concept object to be displayed, eg. {'type': 'TOPIC', 'prefLabel': 'Non-dom tax status', ...}. */
1555
1571
  readonly displayConcept?: Maybe<Concept>;
1556
1572
  /** The variant of the follow button to be displayed on the topper. */
@@ -2403,10 +2419,13 @@ export type ClipSetResolvers<ContextType = QueryContext, ParentType extends Reso
2403
2419
  export type ClipSourceResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['ClipSource'] = ResolversParentTypes['ClipSource']> = ResolversObject<{
2404
2420
  audioCodec: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2405
2421
  binaryUrl: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2422
+ dppx: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
2406
2423
  duration: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
2407
2424
  mediaType: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2408
2425
  pixelHeight: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
2409
2426
  pixelWidth: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
2427
+ previousSourceWidth: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
2428
+ quality: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2410
2429
  videoCodec: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
2411
2430
  __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
2412
2431
  }>;
@@ -2913,6 +2932,7 @@ export type OpinionTopperResolvers<ContextType = QueryContext, ParentType extend
2913
2932
  backgroundBox: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
2914
2933
  backgroundColour: Resolver<Maybe<ResolversTypes['TopperBackgroundColour']>, ParentType, ContextType>;
2915
2934
  columnist: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
2935
+ columnists: Resolver<Maybe<ReadonlyArray<ResolversTypes['Concept']>>, ParentType, ContextType>;
2916
2936
  displayConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
2917
2937
  followButtonVariant: Resolver<Maybe<ResolversTypes['FollowButtonVariant']>, ParentType, ContextType>;
2918
2938
  genreConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
@@ -3041,6 +3061,7 @@ export type PodcastTopperResolvers<ContextType = QueryContext, ParentType extend
3041
3061
  backgroundColour: Resolver<Maybe<ResolversTypes['TopperBackgroundColour']>, ParentType, ContextType>;
3042
3062
  brandConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
3043
3063
  columnist: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
3064
+ columnists: Resolver<Maybe<ReadonlyArray<ResolversTypes['Concept']>>, ParentType, ContextType>;
3044
3065
  displayConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
3045
3066
  followButtonVariant: Resolver<Maybe<ResolversTypes['FollowButtonVariant']>, ParentType, ContextType>;
3046
3067
  genreConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
@@ -0,0 +1,44 @@
1
+ // until we get quality setting data through from CAPI, we infer the quality and ddpx from the details in the filename
2
+ // once we have quality data from CAPI, we can remove this module
3
+ import type { ClipSource } from '../types/clip'
4
+
5
+ // possible output sizes are set in https://github.com/Financial-Times/ip-ovide/tree/main/common/n-zencoder/src/profiles
6
+ const dimensions = new Map([
7
+ [1920, 1080],
8
+ [1280, 720],
9
+ [960, 540],
10
+ [640, 360],
11
+ [480, 270],
12
+ ])
13
+
14
+ // utility to extract width from filename pattern like /640x360.mp4
15
+ function getDimensionsFromFilename(url: string): number[] {
16
+ // regex: get the first set of digits after a "/" and before "x", followed by 1+ digits, followed by ".mp" and 1 digit, then end of string.
17
+ const widthRegex = /\/(\d+)x(\d+)\.mp\d$/i
18
+ const widthMatch = url.match(widthRegex)
19
+ return [
20
+ parseInt(widthMatch?.[1] ?? '0', 10),
21
+ parseInt(widthMatch?.[2] ?? '0', 10),
22
+ ]
23
+ }
24
+
25
+ export default function qualityWorkaround(
26
+ dataSource: ClipSource[]
27
+ ): ClipSource[] {
28
+ return dataSource.map((source) => {
29
+ const originalDimensions = getDimensionsFromFilename(source.binaryUrl ?? '')
30
+ if (originalDimensions[0] === 0) {
31
+ // this will apply to audio files
32
+ return source
33
+ }
34
+ const smallerDimension = Math.min(...originalDimensions)
35
+ source.quality = `${
36
+ Array.from(dimensions.values()).includes(smallerDimension)
37
+ ? smallerDimension
38
+ : dimensions.get(smallerDimension)
39
+ }p`
40
+
41
+ source.dppx = smallerDimension >= 1080 ? 2 : 1
42
+ return source
43
+ })
44
+ }
@@ -4,6 +4,42 @@ const mockContext = {
4
4
  systemCode: 'cp-content-pipeline',
5
5
  } as unknown as QueryContext
6
6
 
7
+ const mockDataSources = [
8
+ {
9
+ audioCodec: 'mp3',
10
+ binaryUrl: 'https://clips.ft.com/someguid/0x0.mp3',
11
+ duration: 21720,
12
+ mediaType: 'audio/mpeg',
13
+ },
14
+ {
15
+ audioCodec: 'aac',
16
+ binaryUrl: 'https://clips.ft.com/someguid/640x360.mp4',
17
+ duration: 21684,
18
+ mediaType: 'video/mp4',
19
+ pixelHeight: 360,
20
+ pixelWidth: 640,
21
+ videoCodec: 'h264',
22
+ },
23
+ {
24
+ audioCodec: 'aac',
25
+ binaryUrl: 'https://clips.ft.com/someguid/1280x720.mp4',
26
+ duration: 21684,
27
+ mediaType: 'video/mp4',
28
+ pixelHeight: 720,
29
+ pixelWidth: 1280,
30
+ videoCodec: 'h264',
31
+ },
32
+ {
33
+ audioCodec: 'aac',
34
+ binaryUrl: 'https://clips.ft.com/someguid/1920x1080.mp4',
35
+ duration: 21684,
36
+ mediaType: 'video/mp4',
37
+ pixelHeight: 720,
38
+ pixelWidth: 1280,
39
+ videoCodec: 'h264',
40
+ },
41
+ ]
42
+
7
43
  describe('Clip', () => {
8
44
  describe('poster', () => {
9
45
  it('uses the Origami Image Service when a url is provided', () => {
@@ -170,4 +206,63 @@ describe('Clip', () => {
170
206
  expect(clip.poster()).toContain('width=1200')
171
207
  })
172
208
  })
209
+ describe('datasources', () => {
210
+ it('creates a quality value and dppx value based on the width in the URL when quality is not provided', () => {
211
+ const clip = new Clip(
212
+ {
213
+ id: 'http://api.ft.com/things/1234',
214
+ type: 'http://www.ft.com/ontology/content/Clip',
215
+ dataSource: mockDataSources,
216
+ },
217
+ mockContext
218
+ )
219
+ const dataSource = clip.dataSource()
220
+ expect(dataSource).toHaveLength(4)
221
+ expect(dataSource?.[0]?.quality).toBe('1080p')
222
+ expect(dataSource?.[0]?.dppx).toBe(2)
223
+ expect(dataSource?.[1]?.quality).toBe('720p')
224
+ expect(dataSource?.[2]?.quality).toBe('360p')
225
+ expect(dataSource?.[3]?.quality).toBeUndefined()
226
+ })
227
+ it('orders the data sources by width and then dppx', () => {
228
+ const clip = new Clip(
229
+ {
230
+ id: 'http://api.ft.com/things/1234',
231
+ type: 'http://www.ft.com/ontology/content/Clip',
232
+ dataSource: mockDataSources,
233
+ },
234
+ mockContext
235
+ )
236
+ const dataSource = clip.dataSource()
237
+ expect(dataSource).toHaveLength(4)
238
+ expect(dataSource?.[0]?.binaryUrl).toBe(
239
+ 'https://clips.ft.com/someguid/1920x1080.mp4'
240
+ )
241
+ expect(dataSource?.[1]?.binaryUrl).toBe(
242
+ 'https://clips.ft.com/someguid/1280x720.mp4'
243
+ )
244
+ expect(dataSource?.[2]?.binaryUrl).toBe(
245
+ 'https://clips.ft.com/someguid/640x360.mp4'
246
+ )
247
+ expect(dataSource?.[3]?.binaryUrl).toBe(
248
+ 'https://clips.ft.com/someguid/0x0.mp3'
249
+ )
250
+ })
251
+ it('sets the previousSourceWidth to the next most narrow source', () => {
252
+ const clip = new Clip(
253
+ {
254
+ id: 'http://api.ft.com/things/1234',
255
+ type: 'http://www.ft.com/ontology/content/Clip',
256
+ dataSource: mockDataSources,
257
+ },
258
+ mockContext
259
+ )
260
+ const dataSource = clip.dataSource()
261
+ expect(dataSource).toHaveLength(4)
262
+ expect(dataSource?.[0]?.previousSourceWidth).toBe(640)
263
+ expect(dataSource?.[1]?.previousSourceWidth).toBe(640)
264
+ expect(dataSource?.[2]?.previousSourceWidth).toBeUndefined()
265
+ expect(dataSource?.[3]?.previousSourceWidth).toBeUndefined()
266
+ })
267
+ })
173
268
  })
package/src/model/Clip.ts CHANGED
@@ -6,25 +6,9 @@ import {
6
6
  } from '../resolvers/literal-union'
7
7
  import { ClipFormat } from '../resolvers/scalars'
8
8
  import imageServiceUrl from '../helpers/imageService'
9
+ import qualityWorkaround from '../helpers/qualityWorkaround'
9
10
  import { QueryContext } from '..'
10
-
11
- type ClipSource = {
12
- binaryUrl: string
13
- mediaType: string
14
- audioCodec?: string
15
- duration?: number
16
- pixelHeight?: number
17
- pixelWidth?: number
18
- videoCodec?: string
19
- }
20
-
21
- interface ClipVideo {
22
- id(): string
23
- type(): string
24
- format(): LiteralUnionScalarValues<typeof ClipFormat>
25
- poster(): string
26
- dataSource(): ClipSource[]
27
- }
11
+ import type { ClipSource, ClipVideo } from '../types/clip'
28
12
 
29
13
  export class Clip implements ClipVideo {
30
14
  constructor(
@@ -60,18 +44,40 @@ export class Clip implements ClipVideo {
60
44
  return 'standard-inline'
61
45
  }
62
46
 
47
+ // apply 'quality' workaround
48
+ // order sources by width and then quality
49
+ // get the min width from the first previous more narrow item
63
50
  dataSource() {
64
- // Order clip dataSource to have an order of video formats first then audio/mpeg, as the browser will first check if it can play the MIME type of the first source
65
- const dataSource = this.clip.dataSource.slice().sort((a, b) => {
66
- if (a.mediaType === 'video/mp4' && b.mediaType !== 'video/mp4') {
67
- return -1
68
- }
69
- if (a.mediaType !== 'video/mp4' && b.mediaType === 'video/mp4') {
70
- return 1
51
+ // create a copy of the array so we don't modify the original data
52
+ let dataSource = this.clip.dataSource.map((src) => ({
53
+ ...src,
54
+ })) as ClipSource[]
55
+ dataSource = qualityWorkaround(dataSource as ClipSource[])
56
+ dataSource = dataSource.sort((a, b) => {
57
+ const widthDiff = (b.pixelWidth ?? 0) - (a.pixelWidth ?? 0)
58
+ if (widthDiff !== 0) {
59
+ return widthDiff
71
60
  }
72
- return 0
61
+ return (b?.dppx ?? 0) - (a?.dppx ?? 0)
73
62
  })
74
- return dataSource as ClipSource[]
63
+
64
+ // get the min width from the previous item if it has pixelWidth set, checking it is not the same as pixelWidth as the current item
65
+ for (let i = 0; i < dataSource.length; i++) {
66
+ let next = 1
67
+ let minWidth = dataSource[i + next]?.pixelWidth
68
+ while (
69
+ typeof minWidth !== 'undefined' &&
70
+ minWidth === dataSource[i]?.pixelWidth
71
+ ) {
72
+ next++
73
+ minWidth = dataSource[i + next]?.pixelWidth
74
+ }
75
+ if (minWidth && dataSource[i]) {
76
+ dataSource[i]!.previousSourceWidth = minWidth
77
+ }
78
+ }
79
+
80
+ return dataSource
75
81
  }
76
82
 
77
83
  poster() {
@@ -289,14 +289,20 @@ export class Topper {
289
289
  return 'standard'
290
290
  }
291
291
 
292
+ // @deprecated Replaced with usage of `columinists`
292
293
  columnist() {
294
+ const authors = this.columnists()
295
+ return authors?.[0] ?? null
296
+ }
297
+
298
+ columnists() {
293
299
  const authors = this.capiResponse.annotations({
294
300
  byPredicate: predicates.hasAuthor,
295
301
  })
296
302
  const isOpinionOrColumn =
297
303
  this.type() === 'OpinionTopper' || this.capiResponse.isColumn()
298
304
 
299
- return isOpinionOrColumn && authors[0] ? authors[0] : null
305
+ return isOpinionOrColumn && authors.length > 0 ? authors : null
300
306
  }
301
307
 
302
308
  brandConcept() {
@@ -103,6 +103,8 @@ const ClipSource = z.object({
103
103
  pixelHeight: z.number().optional(),
104
104
  pixelWidth: z.number().optional(),
105
105
  videoCodec: z.string().optional(),
106
+ quality: z.string().optional(),
107
+ dppx: z.number().optional(),
106
108
  })
107
109
 
108
110
  export const Clip = z.object({
@@ -18,6 +18,9 @@ const resolvers = {
18
18
  pixelWidth: (parent) => parent.pixelWidth ?? null,
19
19
  pixelHeight: (parent) => parent.pixelHeight ?? null,
20
20
  videoCodec: (parent) => parent.videoCodec ?? null,
21
+ quality: (parent) => parent.quality ?? null,
22
+ dppx: (parent) => parent.dppx ?? null,
23
+ previousSourceWidth: (parent) => parent.previousSourceWidth ?? null,
21
24
  },
22
25
  } satisfies { Clip: ClipResolvers; ClipSource: ClipSourceResolvers }
23
26
 
@@ -59,7 +59,8 @@ const resolvers = {
59
59
  headshot: (topper, args) => topper.headshot(args),
60
60
  isLargeHeadline: (topper) => topper.isLargeHeadline(),
61
61
  layout: (topper) => topper.layout(),
62
- columnist: (topper) => topper.columnist(),
62
+ columnist: (topper) => topper.columnist(), // @deprecated Replaced with usage of `columinists`
63
+ columnists: (topper) => topper.columnists(),
63
64
  },
64
65
 
65
66
  OpinionTopper: {
@@ -67,7 +68,8 @@ const resolvers = {
67
68
  headshot: (topper, args) => topper.headshot(args),
68
69
  isLargeHeadline: (topper) => topper.isLargeHeadline(),
69
70
  layout: (topper) => topper.layout(),
70
- columnist: (topper) => topper.columnist(),
71
+ columnist: (topper) => topper.columnist(), // @deprecated Replaced with usage of `columinists`
72
+ columnists: (topper) => topper.columnists(),
71
73
  },
72
74
 
73
75
  TopperWithPackage: {
@@ -0,0 +1,23 @@
1
+ import { LiteralUnionScalarValues } from '../resolvers/literal-union'
2
+ import { ClipFormat } from '../resolvers/scalars'
3
+
4
+ export type ClipSource = {
5
+ binaryUrl: string
6
+ mediaType: string
7
+ audioCodec?: string
8
+ duration?: number
9
+ pixelHeight?: number
10
+ pixelWidth?: number
11
+ videoCodec?: string
12
+ quality?: string
13
+ dppx?: number
14
+ previousSourceWidth?: number
15
+ }
16
+
17
+ export interface ClipVideo {
18
+ id(): string
19
+ type(): string
20
+ format(): LiteralUnionScalarValues<typeof ClipFormat>
21
+ poster(): string
22
+ dataSource(): ClipSource[]
23
+ }