{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://p2present.com/p2present.schema.json",
  "title": "p2present presentation manifest",
  "description": "Schema for a p2present.json v1.0 manifest. See SPEC.md for prose + examples. Every src (video/deck/asset) may be an https URL, an ipfs:// CID, or a magnet: link.",
  "type": "object",
  "required": ["video", "deck"],
  "properties": {
    "p2present": {
      "type": "string",
      "description": "Schema version. Use \"1.0\".",
      "examples": ["1.0"]
    },
    "title": { "type": "string", "description": "Presentation title (shown in the header / document title)." },
    "meta": {
      "type": "object",
      "description": "Optional descriptive metadata.",
      "properties": {
        "author": { "type": "string" },
        "event": { "type": "string" },
        "date": { "type": "string" },
        "description": { "type": "string" }
      },
      "additionalProperties": true
    },
    "video": {
      "type": "object",
      "description": "The talk video. `sources` is an ordered fallback list: the player tries each until one loads.",
      "properties": {
        "sources": {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "object",
            "required": ["provider", "src"],
            "properties": {
              "provider": {
                "type": "string",
                "enum": ["youtube", "mp4", "webtorrent", "ipfs"],
                "description": "youtube (iframe), mp4 (HTML5 video), webtorrent (magnet streamed via file.renderTo), and ipfs (gateway-resolved CID). Sources are an ordered fallback list, so a failed p2p source falls through to the next."
              },
              "src": {
                "type": "string",
                "description": "youtube: video id or watch/youtu.be URL. mp4: URL to the file (resolved against the manifest URL if relative). webtorrent: magnet. ipfs: CID or ipfs:// URI."
              }
            },
            "additionalProperties": true
          }
        },
        "poster": { "type": "string", "description": "Optional poster image URL (used by the mp4 provider)." }
      },
      "additionalProperties": true
    },
    "deck": {
      "type": "object",
      "required": ["type"],
      "description": "The slide deck. `sources` is an ordered fallback list of URLs.",
      "properties": {
        "type": { "type": "string", "enum": ["html", "pdf", "embed"], "description": "Deck adapter to use. \"html\" = an <iframe>'d HTML deck (deck-stage / reveal.js / sections); \"pdf\" = a PDF rendered page-per-slide with pdf.js; \"embed\" = an external embeddable slide URL (Google Slides / SpeakerDeck / Canva …) shown read-only in an <iframe>." },
        "sources": {
          "type": "array",
          "minItems": 1,
          "description": "Ordered fallback list of deck URLs. Each src may be an https URL, an ipfs:// CID, or a magnet: link (the transport is inferred from the scheme).",
          "items": {
            "type": "object",
            "required": ["src"],
            "properties": {
              "src": { "type": "string", "description": "URL to the deck (resolved against the manifest URL if relative)." }
            },
            "additionalProperties": true
          }
        },
        "embed": {
          "type": "object",
          "description": "Optional deep-link config for deck.type \"embed\" only. Lets the player point the embedded slide URL at a given slide; omit for a display-only embed.",
          "properties": {
            "nav": { "type": "string", "enum": ["hash", "query"], "description": "How to encode the slide in the URL: \"hash\" (#param=N, no reload) or \"query\" (?param=N, reloads the frame)." },
            "param": { "type": "string", "description": "URL parameter name carrying the slide number. Default \"slide\"." },
            "offset": { "type": "integer", "description": "Slide value for slide 1 (e.g. 0 for a 0-based embed). Default 1." }
          },
          "additionalProperties": true
        },
        "slideCount": { "type": "integer", "minimum": 1, "description": "Total slide count (used for the counter when the deck can't report it; required for \"embed\" decks, which can't introspect the foreign frame)." },
        "thumbnails": {
          "description": "Optional authored slide thumbnails for the scrubber preview. Either an array of image URLs (indexed by slide, 1-based) or an array of {slide, src}. URLs resolve against the manifest URL if relative. PDF decks auto-render thumbnails when this is omitted; HTML decks fall back to a slide-label card.",
          "oneOf": [
            { "type": "array", "items": { "type": "string" } },
            { "type": "array", "items": { "type": "object", "required": ["slide", "src"], "properties": { "slide": { "type": "integer", "minimum": 1 }, "src": { "type": "string" } }, "additionalProperties": true } }
          ]
        }
      },
      "additionalProperties": true
    },
    "timing": {
      "description": "Slide-boundary cues. Either an inline array of cues OR a string path/URL to an external JSON file (an array, or an object with a `timing` array).",
      "oneOf": [
        {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["slide"],
            "properties": {
              "time": {
                "description": "Cue time in float SECONDS, or a \"HH:MM:SS.mmm\" / \"MM:SS\" string.",
                "type": ["number", "string"]
              },
              "slide": { "type": "integer", "minimum": 1, "description": "1-based slide number." },
              "transition": { "type": "string", "enum": ["cut", "fade", "slide", "none"], "description": "Transition into this slide (default: cut)." },
              "label": { "type": "string", "description": "Optional author note." }
            },
            "additionalProperties": true
          }
        },
        { "type": "string", "description": "Path/URL to an external timing JSON file." }
      ]
    },
    "subtitles": {
      "type": "array",
      "description": "Caption tracks. Rendered as native <track> on mp4, or a synced overlay on YouTube.",
      "items": {
        "type": "object",
        "required": ["src"],
        "properties": {
          "lang": { "type": "string", "description": "BCP-47 language tag (or any unique key)." },
          "label": { "type": "string", "description": "Human label shown in the CC menu." },
          "src": { "type": "string", "description": "URL to the .vtt or .srt file (resolved against the manifest URL if relative)." },
          "format": { "type": "string", "enum": ["vtt", "srt"], "description": "Caption format. Inferred from the file extension if omitted (.srt → srt, else vtt)." },
          "default": { "type": "boolean", "description": "If true, this track is shown by default." }
        },
        "additionalProperties": true
      }
    },
    "resolvers": {
      "type": "object",
      "description": "Override the default networks the phase-2 providers consume.",
      "properties": {
        "ipfsGateways": { "type": "array", "items": { "type": "string" }, "description": "IPFS gateway URL templates; use {cid} as a placeholder." },
        "webtorrentTrackers": { "type": "array", "items": { "type": "string" }, "description": "WebTorrent tracker (wss://) URLs." }
      },
      "additionalProperties": true
    },
    "sig": {
      "type": "object",
      "description": "Optional author signature (Phase 8). Proves who published the manifest; the player verifies it on load and shows a \"✓ signed by …\" badge but NEVER blocks playback. Signed payload = the canonical JSON of the whole manifest including this block's `alg`/`signer`/`canon` but excluding `signature` — so editing the manifest OR the claimed signer invalidates the signature. Add or remove this block freely; an unsigned manifest is fully valid.",
      "required": ["alg", "signer", "signature"],
      "properties": {
        "alg": { "type": "string", "enum": ["eip191", "ed25519"], "description": "\"eip191\" = Ethereum wallet / raw secp256k1 key via the EIP-191 personal_sign prefix (signer is the recovered address — self-authenticating). \"ed25519\" = a raw Ed25519 keypair (the public key is stored in `signer.key`)." },
        "signer": {
          "type": "object",
          "description": "Who signed. eip191 → { address }. ed25519 → { key, domain? }.",
          "properties": {
            "address": { "type": "string", "description": "eip191: the signer's 0x Ethereum address (re-derived from the signature on verify; the stored value is for display + binding)." },
            "key": { "type": "string", "description": "ed25519: the 32-byte raw public key, base64url (no padding)." },
            "domain": { "type": "string", "description": "ed25519: optional human label (e.g. a domain) bound into the signature." },
            "ens": { "type": "string", "description": "Optional cached ENS name for display; the player reverse-resolves live and does not trust this." }
          },
          "additionalProperties": true
        },
        "signature": { "type": "string", "description": "eip191: 0x + r(32) + s(32) + v(1) hex (65 bytes). ed25519: base64url of the 64-byte signature." },
        "canon": { "type": "string", "description": "Canonicalization id. Use \"p2/jcs-1\" (recursively sorted keys, minimal separators, UTF-8)." }
      },
      "additionalProperties": true
    },
    "layout": {
      "type": "object",
      "description": "Default layout. The user's interactive changes are persisted in localStorage and override these.",
      "properties": {
        "split": { "type": "number", "minimum": 0.15, "maximum": 0.85, "description": "Slides-pane fraction of the split (0–1; clamped 0.15–0.85). Default 0.6." },
        "mode": { "type": "string", "enum": ["split", "slides-focus", "video-focus", "overlap"], "description": "Initial layout mode. Default \"split\"." },
        "transition": { "type": "string", "description": "Preferred default slide transition. Default \"fade\"." },
        "captionPlacement": { "type": "string", "enum": ["window", "video"], "description": "Where captions are drawn: \"window\" (default) overlays them along the bottom of the WHOLE player — slides + video, every layout mode incl. fullscreen; \"video\" keeps them inside the video pane (native <track> for mp4). The user can override this in the Subtitles menu (persisted in localStorage)." }
      },
      "additionalProperties": true
    }
  },
  "additionalProperties": true
}
